Skip to content

context

context se traduce como contexto. Fue diseñado originalmente para transmitir señales y algunos datos simples entre múltiples goroutines, especialmente entre goroutines padre e hijo. Se usa comúnmente en escenarios como manejo de solicitudes HTTP, planificación de tareas, consultas de bases de datos, especialmente en arquitecturas de microservicios, donde gRPC usa context para transmitir metadatos entre procesos y redes, control de trazas, etc.

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

En este ejemplo, context se usa para transmitir señales de proceso. Cuando se recibe una señal, el programa termina por sí mismo. Esta es también una de las aplicaciones de context.

Estructura

context.Context no es una implementación concreta, sino una interfaz que define un conjunto de métodos:

go
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline(), retorna una fecha límite y un valor booleano que indica si ha terminado
  • Done(), retorna un canal usado para notificar el mensaje de finalización
  • Err(), retorna la razón del cierre del contexto
  • Value(), obtiene el valor especificado según la clave

En la biblioteca estándar hay varios contextos disponibles:

  • Background, contexto vacío, usado comúnmente como nodo raíz del contexto
  • WithCancel, WithCancelCause, contexto cancelable
  • WithDeadline, WithDeadlineCause, contexto con fecha límite
  • WithTimeout, WithTimeoutCause, contexto con tiempo de espera
  • WithValue, contexto que puede transmitir valores

En cuanto a la implementación, principalmente son:

  • timerCtx
  • cancelCtx
  • emptyCtx

Por lo tanto, su funcionalidad central tiene cuatro puntos:

  • Cancelación
  • Fecha límite
  • Transmisión de valores
  • Propagación

Entender estos puntos básicamente explica cómo funciona context.

Cancelación

go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protege los siguientes campos
	done     atomic.Value          // de chan struct{}, creado perezosamente, cerrado por la primera llamada a cancel
	children map[canceler]struct{} // establecido a nil por la primera llamada a cancel
	err      error                 // establecido a no-nil por la primera llamada a cancel
	cause    error                 // establecido a no-nil por la primera llamada a cancel
}


func withCancel(parent Context) *cancelCtx {
    if parent == nil {
       panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

El núcleo de cancelCtx radica en el método propagateCancel, que es responsable de propagar el comportamiento de cancelación a los contextos padre e hijo.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Primero verifica si el contexto padre puede ser cancelado. Si no, retorna directamente.

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Verifica si el contexto padre ya ha sido cancelado. Si es así, cancela todos los contextos hijos.

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Intenta convertir el contexto padre al tipo cancelCtx. Si tiene éxito, agrega el contexto actual a los children del contexto padre.

    go
    if p, ok := parentCancelCtx(parent); ok {
    	// parent is a *cancelCtx, or derives from one.
    	p.mu.Lock()
    	if p.err != nil {
    		// parent has already been canceled
    		child.cancel(false, p.err, p.cause)
    	} else {
    		if p.children == nil {
    			p.children = make(map[canceler]struct{})
    		}
    		p.children[child] = struct{}{}
    	}
    	p.mu.Unlock()
    	return
    }
  4. Intenta convertirlo al tipo afterFuncer. Si tiene éxito, registra el método de cancelación del contexto actual en el AfterFunc del contexto padre.

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent implements an AfterFunc method.
    	c.mu.Lock()
    	stop := a.AfterFunc(func() {
    		child.cancel(false, parent.Err(), Cause(parent))
    	})
    	c.Context = stopCtx{
    		Context: parent,
    		stop:    stop,
    	}
    	c.mu.Unlock()
    	return
    }
  5. Si aún no funciona, inicia una goroutine separada para monitorear el canal Done. Cuando recibe la señal, cancela el contexto hijo.

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

Luego, el método cancelCtx.cancel es finalmente responsable de cancelar los contextos hijos:

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

Su flujo es el siguiente:

  1. Verifica si el contexto actual ya ha sido cancelado.

    go
    if err == nil {
    	panic("context: internal error: missing cancel error")
    }
    if cause == nil {
    	cause = err
    }
    c.mu.Lock()
    if c.err != nil {
    	c.mu.Unlock()
    	return // already canceled
    }
  2. Cierra el canal done, enviando la notificación de cierre.

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Itera y notifica a los contextos hijos.

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Finalmente, según el parámetro传入, determina si necesita eliminarse del contexto padre.

    if removeFromParent {
    	removeChild(c.Context, c)
    }

Fecha Límite

Tanto WithTimeout como WithDeadline son contextos con fecha límite. Ambos son del mismo tipo, solo difieren en la semántica de uso, y ambos se basan en cancelCtx.

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Bajo cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause es responsable de crear contextos con fecha límite:

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

Su flujo es el siguiente:

  1. Verifica la fecha límite. Si la fecha límite del padre es anterior a la fecha límite actual, entonces el contexto padre definitivamente se cancelará antes que el contexto actual, por lo que crea directamente un contexto del tipo cancelCtx.

    go
    if parent == nil {
    	panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    	// The current deadline is already sooner than the new one.
    	return WithCancel(parent)
    }
  2. Construye timerCtx y lo propaga a los contextos hijos.

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Calcula el tiempo de vencimiento actual. Si ya venció, cancela directamente.

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. Si no ha vencido, usa time.AfterFunc para configurar la cancelación del contexto actual en el momento de vencimiento.

    go
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    	c.timer = time.AfterFunc(dur, func() {
    		c.cancel(true, DeadlineExceeded, cause)
    	})
    }
    return c, func() { c.cancel(true, Canceled, nil) }

Para timerCtx, su método de cancelación de contexto solo detiene el timer y también detiene los contextos hijos.

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

Transmisión de Valores

valueCtx puede transmitir y obtener valores en los contextos:

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

Su núcleo radica en la función value. Otras implementaciones concretas de contextos también entrarán en esta función para obtener valores, por ejemplo:

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

La función value es un gran bucle for que busca recursivamente hacia arriba para encontrar el valor de la clave especificada:

go
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

Manejo de diferentes tipos de Context:

  • *valueCtx: Si key coincide con la key del Contexto actual, retorna el val correspondiente a la key.
  • *cancelCtx: Si key coincide con cancelCtxKey, retorna el cancelCtx actual, es decir, se retorna a sí mismo.
  • withoutCancelCtx: Representa un Context sin función de cancelación. Si key coincide con cancelCtxKey, retorna nil.
  • *timerCtx: Si key coincide con cancelCtxKey, retorna el cancelCtx asociado.
  • backgroundCtx y todoCtx: Son tipos especiales de Context que normalmente no携带 ningún valor adicional. Al encontrar estos dos tipos, retorna directamente nil.
  • Si es un tipo desconocido, continúa llamando al método Value para buscar.

Resumen

El núcleo de estos contextos es cancelCtx, que propaga la señal de cancelación. Otros tipos de contextos, así como algunos contextos de terceros, se envuelven capa por capa, implementando diversas funciones sobre esta base.

Golang editado por www.golangdev.cn