context
context se traduit par contexte. Sa conception initiale était de transmettre des signaux et quelques données simples entre plusieurs coroutines, en particulier entre coroutines parentes et enfants. Il est généralement utilisé dans le traitement des requêtes HTTP, l'ordonnancement de tâches, les requêtes de base de données, etc. Surtout dans les architectures de microservices, gRPC utilise context pour transmettre des métadonnées et contrôler les chaînes d'appels entre processus et sur le réseau.
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)
}
}Dans cet exemple, context est utilisé pour transmettre des signaux de processus. Lorsqu'un signal est reçu, le programme se termine automatiquement, c'est aussi un cas d'utilisation de context.
Structure
context.Context n'est pas une implémentation concrète, mais une interface définissant un ensemble de méthodes.
// 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(), retourne une date limite et une valeur booléenne indiquant si elle est terminéeDone(), retourne un canal pour notifier les messages de finErr(), retourne la raison de la fermeture du contexteValue(), récupère la valeur spécifiée selon la clé
La bibliothèque standard contient plusieurs contextes disponibles :
Background, contexte vide, généralement utilisé comme nœud racine du contexteWithCancel,WithCancelCause, contexte annulableWithDeadline,WithDeadlineCause, contexte avec date limiteWithTimeout,WithTimeoutCause, contexte avec délai d'expirationWithValue, contexte pouvant transmettre des valeurs
Les implémentations concrètes sont principalement :
timerCtxcancelCtxemptyCtx
Ses fonctionnalités principales sont donc :
- Annulation
- Date limite
- Transmission de valeurs
- Propagation
Une fois ces points compris, on comprend essentiellement le principe de fonctionnement de context.
Annulation
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
}Le cœur de cancelCtx réside dans la méthode propagateCancel, qui est responsable de propager le comportement d'annulation aux contextes parent et enfant.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}D'abord, elle vérifie si le contexte parent peut être annulé, sinon elle retourne directement
godone := parent.Done() if done == nil { return // parent is never canceled }Vérifie si le contexte parent a déjà été annulé, si oui alors annule tous les contextes enfants
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Essaie de convertir le contexte parent en type
cancelCtx, si réussi, ajoute le contexte courant auxchildrendu contexte parent.Essaie de le convertir en type
afterFuncer, si réussi, enregistre la méthode d'annulation du contexte courant dans leAfterFuncdu contexte parent.Si toujours pas possible, lance une coroutine séparée pour surveiller le canal
Done, et annule le contexte enfant quand le signal est reçu.
Ensuite, la méthode cancelCtx.cancel est responsable d'annuler le contexte enfant.
Date limite
WithTimeout et WithDeadline sont des contextes avec date limite. Les deux sont du même type, seule la sémantique d'utilisation diffère, et tous deux sont basés sur cancelCtx.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause est responsable de créer un contexte avec date limite.
Pour timerCtx, sa méthode d'annulation se contente d'arrêter le timer, puis d'arrêter le contexte enfant.
Transmission de valeurs
valueCtx peut transmettre et récupérer des valeurs dans le contexte.
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}
}Son cœur réside dans la fonction value. Les autres implémentations concrètes de contexte entrent aussi dans cette fonction pour récupérer des valeurs.
La fonction value est une grande boucle for qui recherche la valeur de la clé spécifiée par récursion vers le haut.
Traitement des différents types de Context :
*valueCtx: sikeycorrespond à lakeyduContextcourant, retourne levalassocié àkey.*cancelCtx: sikeycorrespond àcancelCtxKey, retourne lecancelCtxcourant.withoutCancelCtx: représente unContextsans fonction d'annulation, sikeycorrespond àcancelCtxKey, retournenil.*timerCtx: sikeycorrespond àcancelCtxKey, retourne lecancelCtxassocié.backgroundCtxettodoCtx: ce sont des types spéciaux deContextne portant aucune valeur supplémentaire, retournenildirectement.- Si c'est un type inconnu, continue à appeler la méthode
Valuepour chercher.
Résumé
Voici le processus d'annulation de cancelCtx.cancel :
Vérifier l'erreur et la cause, puis verrouiller
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 }Fermer le canal
done, envoyer une notification de fermeturegoc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Parcourir et notifier les contextes enfants
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Selon le paramètre passé, déterminer s'il faut supprimer du contexte parent
goif removeFromParent { removeChild(c.Context, c) }
Date limite
WithTimeout et WithDeadline sont tous deux des contextes avec une date limite. Ils sont du même type, seule la sémantique d'utilisation diffère, et tous deux sont basés sur cancelCtx.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause est responsable de créer un contexte avec une date limite.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Son processus est le suivant :
Vérifier la date limite. Si la date limite du parent est antérieure à la date limite actuelle, alors le contexte parent sera certainement annulé avant le contexte actuel, donc on crée directement un contexte de type
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) }Construire
timeCtx, et propager au contexte enfantgoc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Calculer le temps limite actuel, si la date limite est déjà passée, annuler directement
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 la date limite n'est pas encore passée, utiliser
time.AfterFuncpour programmer l'annulation du contexte actuel à la date limitegoc.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) }
Pour timerCtx, sa méthode d'annulation se contente d'arrêter le timer, puis d'arrêter le contexte enfant.
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()
}Transmission de valeurs
valueCtx peut transmettre et récupérer des valeurs dans le contexte.
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}
}Son cœur réside dans la fonction value. Les autres implémentations concrètes de contexte entrent aussi dans cette fonction pour récupérer des valeurs, par exemple :
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 fonction value est une grande boucle for qui recherche la valeur de la clé spécifiée par récursion vers le haut.
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)
}
}
}Traitement des différents types de Context :
*valueCtx: sikeycorrespond à lakeyduContextcourant, retourne levalassocié àkey.*cancelCtx: sikeycorrespond àcancelCtxKey, retourne lecancelCtxcourant, c'est-à-dire lui-même.withoutCancelCtx: représente unContextsans fonction d'annulation, sikeycorrespond àcancelCtxKey, retournenil.*timerCtx: sikeycorrespond àcancelCtxKey, retourne lecancelCtxassocié.backgroundCtxettodoCtx: ce sont des types spéciaux deContextne portant aucune valeur supplémentaire, retournenildirectement.- Si c'est un type inconnu, continue à appeler la méthode
Valuepour chercher.
Résumé
Le cœur de ces contextes est cancelCtx, qui permet de propager les signaux d'annulation. Les autres types de contexte ainsi que certains contextes tiers sont également construits couche par couche, implémentant diverses fonctionnalités sur cette base.
