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.
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:
// 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 terminadoDone(), retorna un canal usado para notificar el mensaje de finalizaciónErr(), retorna la razón del cierre del contextoValue(), 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 contextoWithCancel,WithCancelCause, contexto cancelableWithDeadline,WithDeadlineCause, contexto con fecha límiteWithTimeout,WithTimeoutCause, contexto con tiempo de esperaWithValue, contexto que puede transmitir valores
En cuanto a la implementación, principalmente son:
timerCtxcancelCtxemptyCtx
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
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.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Primero verifica si el contexto padre puede ser cancelado. Si no, retorna directamente.
godone := parent.Done() if done == nil { return // parent is never canceled }Verifica si el contexto padre ya ha sido cancelado. Si es así, cancela todos los contextos hijos.
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Intenta convertir el contexto padre al tipo
cancelCtx. Si tiene éxito, agrega el contexto actual a loschildrendel contexto padre.goif 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 }Intenta convertirlo al tipo
afterFuncer. Si tiene éxito, registra el método de cancelación del contexto actual en elAfterFuncdel contexto padre.goif 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 }Si aún no funciona, inicia una goroutine separada para monitorear el canal
Done. Cuando recibe la señal, cancela el contexto hijo.gogo 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:
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Su flujo es el siguiente:
Verifica si el contexto actual ya ha sido cancelado.
goif 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 }Cierra el canal
done, enviando la notificación de cierre.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Itera y notifica a los contextos hijos.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()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.
type timerCtx struct {
cancelCtx
timer *time.Timer // Bajo cancelCtx.mu.
deadline time.Time
}WithDeadlineCause es responsable de crear contextos con fecha límite:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Su flujo es el siguiente:
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.goif 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) }Construye
timerCtxy lo propaga a los contextos hijos.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Calcula el tiempo de vencimiento actual. Si ya venció, cancela directamente.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Si no ha vencido, usa
time.AfterFuncpara configurar la cancelación del contexto actual en el momento de vencimiento.goc.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.
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:
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:
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:
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: Sikeycoincide con lakeydel Contexto actual, retorna elvalcorrespondiente a lakey.*cancelCtx: Sikeycoincide concancelCtxKey, retorna elcancelCtxactual, es decir, se retorna a sí mismo.withoutCancelCtx: Representa unContextsin función de cancelación. Sikeycoincide concancelCtxKey, retornanil.*timerCtx: Sikeycoincide concancelCtxKey, retorna elcancelCtxasociado.backgroundCtxytodoCtx: Son tipos especiales deContextque normalmente no携带 ningún valor adicional. Al encontrar estos dos tipos, retorna directamentenil.- Si es un tipo desconocido, continúa llamando al método
Valuepara 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.
