Skip to content

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.

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)
    }
}

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.

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(), retourne une date limite et une valeur booléenne indiquant si elle est terminée
  • Done(), retourne un canal pour notifier les messages de fin
  • Err(), retourne la raison de la fermeture du contexte
  • Value(), 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 contexte
  • WithCancel, WithCancelCause, contexte annulable
  • WithDeadline, WithDeadlineCause, contexte avec date limite
  • WithTimeout, WithTimeoutCause, contexte avec délai d'expiration
  • WithValue, contexte pouvant transmettre des valeurs

Les implémentations concrètes sont principalement :

  • timerCtx
  • cancelCtx
  • emptyCtx

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

go
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.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. D'abord, elle vérifie si le contexte parent peut être annulé, sinon elle retourne directement

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Vérifie si le contexte parent a déjà été annulé, si oui alors annule tous les contextes enfants

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Essaie de convertir le contexte parent en type cancelCtx, si réussi, ajoute le contexte courant aux children du contexte parent.

  4. Essaie de le convertir en type afterFuncer, si réussi, enregistre la méthode d'annulation du contexte courant dans le AfterFunc du contexte parent.

  5. 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.

go
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.

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}
}

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 : si key correspond à la key du Context courant, retourne le val associé à key.
  • *cancelCtx : si key correspond à cancelCtxKey, retourne le cancelCtx courant.
  • withoutCancelCtx : représente un Context sans fonction d'annulation, si key correspond à cancelCtxKey, retourne nil.
  • *timerCtx : si key correspond à cancelCtxKey, retourne le cancelCtx associé.
  • backgroundCtx et todoCtx : ce sont des types spéciaux de Context ne portant aucune valeur supplémentaire, retourne nil directement.
  • Si c'est un type inconnu, continue à appeler la méthode Value pour chercher.

Résumé

Voici le processus d'annulation de cancelCtx.cancel :

  1. Vérifier l'erreur et la cause, puis verrouiller

    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. Fermer le canal done, envoyer une notification de fermeture

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Parcourir et notifier les contextes enfants

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. Selon le paramètre passé, déterminer s'il faut supprimer du contexte parent

    go
    if 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.

go
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.

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

Son processus est le suivant :

  1. 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.

    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. Construire timeCtx, et propager au contexte enfant

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Calculer le temps limite actuel, si la date limite est déjà passée, annuler directement

    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 la date limite n'est pas encore passée, utiliser time.AfterFunc pour programmer l'annulation du contexte actuel à la date limite

    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) }

Pour timerCtx, sa méthode d'annulation se contente d'arrêter le timer, puis d'arrêter le contexte enfant.

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()
}

Transmission de valeurs

valueCtx peut transmettre et récupérer des valeurs dans le contexte.

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}
}

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 :

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 fonction value est une grande boucle for qui recherche la valeur de la clé spécifiée par récursion vers le haut.

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)
		}
	}
}

Traitement des différents types de Context :

  • *valueCtx : si key correspond à la key du Context courant, retourne le val associé à key.
  • *cancelCtx : si key correspond à cancelCtxKey, retourne le cancelCtx courant, c'est-à-dire lui-même.
  • withoutCancelCtx : représente un Context sans fonction d'annulation, si key correspond à cancelCtxKey, retourne nil.
  • *timerCtx : si key correspond à cancelCtxKey, retourne le cancelCtx associé.
  • backgroundCtx et todoCtx : ce sont des types spéciaux de Context ne portant aucune valeur supplémentaire, retourne nil directement.
  • Si c'est un type inconnu, continue à appeler la méthode Value pour 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.

Golang by www.golangdev.cn edit