context
context wird als Kontext übersetzt. Der ursprüngliche Entwurf diente dazu, Signale und einfache Daten über mehrere Koroutinen hinweg zu übertragen, insbesondere zwischen Eltern- und Kind-Koroutinen. Es wird häufig in Szenarien wie der Verarbeitung von HTTP-Anfragen, Aufgabenplanung, Datenbankabfragen usw. verwendet, besonders in Microservice-Architekturen. gRPC verwendet context für Metadatenübertragung und Ablaufsteuerung über Prozesse und Netzwerke hinweg.
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)
}
}Im obigen Beispiel wird context verwendet, um Prozesssignale zu übertragen. Wenn ein Signal empfangen wird, beendet sich das Programm selbstständig. Dies ist ein Anwendungsszenario von context.
Struktur
context.Context ist keine konkrete Implementierung, sondern ein Interface, das eine Reihe von Methoden definiert:
// 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(): Gibt eine Deadline und einen booleschen Wert zurück, der angibt, ob sie bereits beendet istDone(): Gibt einen Channel zurück, der zum Senden von Beendigungsnachrichten verwendet wirdErr(): Gibt den Grund für das Schließen des Kontexts zurückValue(): Gibt den Wert für einen angegebenen Schlüssel zurück
In der Standardbibliothek gibt es folgende verfügbare Kontexte:
Background: Leerer Kontext, normalerweise als Wurzelknoten des Kontexts verwendetWithCancel,WithCancelCause: Abbrechbarer KontextWithDeadline,WithDeadlineCause: Kontext mit DeadlineWithTimeout,WithTimeoutCause: Kontext mit TimeoutWithValue: Kontext, der Werte übertragen kann
Die konkreten Implementierungen sind hauptsächlich:
timerCtxcancelCtxemptyCtx
Die Kernfunktionen sind also vier Punkte:
- Abbruch
- Deadline
- Wertübertragung
- Weitergabe
Wenn man diese Punkte versteht, versteht man im Wesentlichen, wie context funktioniert.
Abbruch
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
}Der Kern von cancelCtx liegt in der Methode propagateCancel, die dafür verantwortlich ist, das Abbruchverhalten an Eltern- und Kind-Kontexte weiterzugeben.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}Zuerst wird geprüft, ob der Eltern-Kontext abgebrochen werden kann. Wenn nicht, wird direkt zurückgekehrt:
godone := parent.Done() if done == nil { return // parent is never canceled }Dann wird geprüft, ob der Eltern-Kontext bereits abgebrochen wurde. Falls ja, werden alle Kind-Kontexte abgebrochen:
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }Es wird versucht, den Eltern-Kontext in den Typ
cancelCtxumzuwandeln. Bei Erfolg wird der aktuelle Kontext zu denchildrendes Eltern-Kontexts hinzugefügt: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 }Es wird versucht, ihn in den Typ
afterFuncerumzuwandeln. Bei Erfolg wird die Methode zum Abbrechen des aktuellen Kontexts imAfterFuncdes Eltern-Kontexts registriert: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 }Wenn dies auch nicht möglich ist, wird eine separate Goroutine gestartet, um den
Done-Channel zu überwachen. Wenn ein Signal empfangen wird, wird der Kind-Kontext abgebrochen:gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
Die Methode cancelCtx.cancel ist schließlich dafür verantwortlich, den Kind-Kontext abzubrechen:
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}Der Ablauf ist wie folgt:
Prüfen, ob der aktuelle Kontext bereits abgebrochen wurde:
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 }Schließen des
done-Channels, Senden der Schließungsbenachrichtigung:goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }Durchlaufen und Benachrichtigen der Kind-Kontexte:
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()Basierend auf dem übergebenen Parameter entscheiden, ob aus dem Eltern-Kontext gelöscht werden soll:
if removeFromParent { removeChild(c.Context, c) }
Deadline
WithTimeout und WithDeadline sind beide Kontexte mit einer Deadline. Beide sind vom selben Typ, unterscheiden sich nur in der semantischen Verwendung und basieren beide auf cancelCtx:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause ist für das Erstellen von Kontexten mit Deadline verantwortlich:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}Der Ablauf ist wie folgt:
Prüfen der Deadline. Wenn die Eltern-Deadline früher als die aktuelle Deadline ist, wird der Eltern-Kontext definitiv früher als der aktuelle Kontext abgebrochen. In diesem Fall wird direkt ein Kontext vom Typ
cancelCtxerstellt: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) }Erstellen von
timeCtxund Weitergabe an den Kind-Kontext:goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)Berechnen der aktuellen Deadline. Wenn sie bereits erreicht ist, wird direkt abgebrochen:
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }Wenn die Deadline noch nicht erreicht ist, wird durch
time.AfterFunceingestellt, dass der aktuelle Kontext zum Deadline-Zeitpunkt abgebrochen wird: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) }
Für timerCtx besteht die Abbruchmethode lediglich darin, den timer zu stoppen und dann auch den Kind-Kontext zu stoppen:
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()
}Wertübertragung
valueCtx kann Werte im Kontext übertragen und abrufen:
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}
}Der Kern liegt in der Funktion value. Andere konkrete Implementierungen von context rufen diese Funktion ebenfalls auf, um Werte abzurufen, zum Beispiel:
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)
}Die Funktion value ist eine große for-Schleife, die durch ständiges Aufwärts-Rekursieren den Wert für den angegebenen Schlüssel sucht:
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)
}
}
}Behandlung verschiedener Context-Typen:
*valueCtx: Wennkeymit demkeydes aktuellenContextübereinstimmt, wird der zukeygehörendevalzurückgegeben.*cancelCtx: WennkeymitcancelCtxKeyübereinstimmt, wird der aktuellecancelCtxzurückgegeben, was bedeutet, dass er sich selbst zurückgibt.withoutCancelCtx: Stellt einenContextohne Abbruchfunktionalität dar. WennkeymitcancelCtxKeyübereinstimmt, wirdnilzurückgegeben.*timerCtx: WennkeymitcancelCtxKeyübereinstimmt, wird der zugehörigecancelCtxzurückgegeben.backgroundCtxundtodoCtx: Dies sind spezielle Typen vonContext, die normalerweise keine zusätzlichen Werte tragen. Bei diesen beiden Typen wird direktnilzurückgegeben.- Bei unbekannten Typen wird weiterhin die
Value-Methode aufgerufen, um zu suchen.
Zusammenfassung
Der Kern dieser Kontexte ist cancelCtx, durch das Abbruchsignale weitergegeben werden. Andere Kontexttypen und einige Drittanbieter-Kontexttypen sind ebenfalls schichtweise aufgebaut und implementieren auf dieser Basis verschiedene Funktionen.
