context
context tradotto come contesto, è stato progettato inizialmente per trasmettere segnali e alcuni dati semplici tra più goroutine, specialmente tra goroutine padre e figlio. Viene comunemente utilizzato nella gestione di richieste HTTP, schedulazione di task, query di database e altri scenari, specialmente nell'architettura dei microservizi, dove gRPC utilizza context per la trasmissione di metadati tra processi e reti, controllo della catena di chiamate e altre operazioni.
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)
}
}In questo esempio, context viene utilizzato per trasmettere i segnali del processo. Quando viene ricevuto un segnale, il programma si chiude automaticamente. Questo è uno degli scenari d'uso di context.
Struttura
context.Context non è un'implementazione concreta, ma un'interfaccia che definisce un insieme di metodi:
// 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(), restituisce una data di scadenza e un valore booleano che indica se è terminatoDone(), restituisce un channel utilizzato per notificare il messaggio di terminazioneErr(), restituisce il motivo della chiusura del contextValue(), restituisce il valore specificato in base alla chiave
Nella libreria standard ci sono i seguenti context utilizzabili:
Background, context vuoto, comunemente utilizzato come nodo radice dei contextWithCancel,WithCancelCause, context cancellabileWithDeadline,WithDeadlineCause, context con data di scadenzaWithTimeout,WithTimeoutCause, context con timeoutWithValue, context che può trasmettere valori
Le implementazioni concrete sono principalmente:
timerCtxcancelCtxemptyCtx
Quindi le sue funzionalità principali sono quattro punti:
- Cancellazione
- Scadenza
- Trasmissione di valori
- Propagazione
Comprendendo questi punti, si può capire il principio di funzionamento di context.
Cancellazione
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}Il cuore di cancelCtx risiede nel metodo propagateCancel, che è responsabile della propagazione del comportamento di cancellazione ai context padre e figlio.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Per prima cosa verifica se il context padre può essere cancellato, se non può, ritorna direttamente.
godone := parent.Done() if done == nil { return // parent is never canceled }Verifica se il context padre è già stato cancellato, se sì, cancella tutti i context figlio.
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Tenta di convertire il context padre in tipo
cancelCtx, se la conversione ha successo, aggiunge il context corrente aichildrendel context 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 }Tenta di convertirlo in tipo
afterFuncer, se la conversione ha successo, registra il metodo di cancellazione del context corrente nell'AfterFuncdel context 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 }Se ancora non funziona, avvia una goroutine separata per ascoltare il channel
Done, quando riceve il segnale, cancella il context figlio.gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Poi il metodo cancelCtx.cancel è responsabile della cancellazione finale dei context figlio.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Il suo flusso è il seguente:
Verifica se il context corrente è già stato cancellato.
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 }Chiude il channel
done, invia la notifica di chiusura.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Itera per notificare i context figlio.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Infine, in base al parametro传入, decide se rimuovere dal context padre.
goif removeFromParent { removeChild(c.Context, c) }
Scadenza
Sia WithTimeout che WithDeadline sono context con scadenza, entrambi sono dello stesso tipo, differiscono solo semanticamente, e sono entrambi basati su cancelCtx.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause è responsabile della creazione di context con scadenza.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Il suo flusso è il seguente:
Verifica la data di scadenza, se la data di scadenza del padre è precedente alla data di scadenza corrente, allora il context padre verrà sicuramente cancellato prima del context corrente, quindi crea direttamente un context di 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) }Costruisce
timerCtxe propaga ai context figlio.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Calcola il tempo di scadenza corrente, se è già scaduto, lo cancella direttamente.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Se non è scaduto, utilizza
time.AfterFuncper impostare la cancellazione del context corrente al tempo di scadenza.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) }
Per timerCtx, il suo metodo di cancellazione del context si limita a fermare il timer, e顺便 ferma i context figlio.
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()
}Trasmissione di Valori
valueCtx può trasmettere e ottenere valori nei context.
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}
}Il suo cuore risiede nella funzione value, anche le altre implementazioni concrete di context entrano in questa funzione per ottenere i valori, ad esempio:
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 funzione value è un grande ciclo for, che cerca ricorsivamente verso l'alto il valore della chiave specificata.
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)
}
}
}Gestione di diversi tipi Context:
*valueCtx: Se lakeycorrisponde allakeydel Context corrente, restituisce ilvalcorrispondente allakey.*cancelCtx: Se lakeycorrisponde acancelCtxKey, restituisce ilcancelCtxcorrente, indicando se stesso.withoutCancelCtx: Rappresenta unContextsenza funzionalità di cancellazione, se lakeycorrisponde acancelCtxKey, restituiscenil.*timerCtx: Se lakeycorrisponde acancelCtxKey, restituisce ilcancelCtxassociato.backgroundCtxetodoCtx: Sono tipi speciali diContextche non contengono valori extra, restituiscono direttamentenilquando incontrati.- Se è un tipo sconosciuto, continua a chiamare il metodo
Valueper cercare.
Riepilogo
Questi context hanno come cuore cancelCtx, che propaga i segnali di cancellazione. Gli altri tipi di context, inclusi alcuni context di terze parti, sono stratificati uno sull'altro, implementando varie funzionalità su questa base.
