Skip to content

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.

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

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:

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(): Gibt eine Deadline und einen booleschen Wert zurück, der angibt, ob sie bereits beendet ist
  • Done(): Gibt einen Channel zurück, der zum Senden von Beendigungsnachrichten verwendet wird
  • Err(): Gibt den Grund für das Schließen des Kontexts zurück
  • Value(): 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 verwendet
  • WithCancel, WithCancelCause: Abbrechbarer Kontext
  • WithDeadline, WithDeadlineCause: Kontext mit Deadline
  • WithTimeout, WithTimeoutCause: Kontext mit Timeout
  • WithValue: Kontext, der Werte übertragen kann

Die konkreten Implementierungen sind hauptsächlich:

  • timerCtx
  • cancelCtx
  • emptyCtx

Die Kernfunktionen sind also vier Punkte:

  • Abbruch
  • Deadline
  • Wertübertragung
  • Weitergabe

Wenn man diese Punkte versteht, versteht man im Wesentlichen, wie context funktioniert.

Abbruch

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
}

Der Kern von cancelCtx liegt in der Methode propagateCancel, die dafür verantwortlich ist, das Abbruchverhalten an Eltern- und Kind-Kontexte weiterzugeben.

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. Zuerst wird geprüft, ob der Eltern-Kontext abgebrochen werden kann. Wenn nicht, wird direkt zurückgekehrt:

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. Dann wird geprüft, ob der Eltern-Kontext bereits abgebrochen wurde. Falls ja, werden alle Kind-Kontexte abgebrochen:

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. Es wird versucht, den Eltern-Kontext in den Typ cancelCtx umzuwandeln. Bei Erfolg wird der aktuelle Kontext zu den children des Eltern-Kontexts hinzugefügt:

    go
    if 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
    }
  4. Es wird versucht, ihn in den Typ afterFuncer umzuwandeln. Bei Erfolg wird die Methode zum Abbrechen des aktuellen Kontexts im AfterFunc des Eltern-Kontexts registriert:

    go
    if 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
    }
  5. 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:

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

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

Der Ablauf ist wie folgt:

  1. Prüfen, ob der aktuelle Kontext bereits abgebrochen wurde:

    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. Schließen des done-Channels, Senden der Schließungsbenachrichtigung:

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. Durchlaufen und Benachrichtigen der Kind-Kontexte:

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. 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:

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause ist für das Erstellen von Kontexten mit Deadline verantwortlich:

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

Der Ablauf ist wie folgt:

  1. 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 cancelCtx erstellt:

    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. Erstellen von timeCtx und Weitergabe an den Kind-Kontext:

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. Berechnen der aktuellen Deadline. Wenn sie bereits erreicht ist, wird direkt abgebrochen:

    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. Wenn die Deadline noch nicht erreicht ist, wird durch time.AfterFunc eingestellt, dass der aktuelle Kontext zum Deadline-Zeitpunkt abgebrochen wird:

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

Für timerCtx besteht die Abbruchmethode lediglich darin, den timer zu stoppen und dann auch den Kind-Kontext zu stoppen:

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

Wertübertragung

valueCtx kann Werte im Kontext übertragen und abrufen:

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

Der Kern liegt in der Funktion value. Andere konkrete Implementierungen von context rufen diese Funktion ebenfalls auf, um Werte abzurufen, zum Beispiel:

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

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:

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

Behandlung verschiedener Context-Typen:

  • *valueCtx: Wenn key mit dem key des aktuellen Context übereinstimmt, wird der zu key gehörende val zurückgegeben.
  • *cancelCtx: Wenn key mit cancelCtxKey übereinstimmt, wird der aktuelle cancelCtx zurückgegeben, was bedeutet, dass er sich selbst zurückgibt.
  • withoutCancelCtx: Stellt einen Context ohne Abbruchfunktionalität dar. Wenn key mit cancelCtxKey übereinstimmt, wird nil zurückgegeben.
  • *timerCtx: Wenn key mit cancelCtxKey übereinstimmt, wird der zugehörige cancelCtx zurückgegeben.
  • backgroundCtx und todoCtx: Dies sind spezielle Typen von Context, die normalerweise keine zusätzlichen Werte tragen. Bei diesen beiden Typen wird direkt nil zurü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.

Golang by www.golangdev.cn edit