Golang Context Deep Dive: From Zero to Hero
Olivia Novak
Dev Intern · Leapcell

1. Was ist Context?
Einfach ausgedrückt ist Context eine Schnittstelle in der Standardbibliothek, die in Go Version 1.7 eingeführt wurde. Ihre Definition lautet wie folgt:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Diese Schnittstelle definiert vier Methoden:
- Deadline: Legt den Zeitpunkt fest, zu dem der
context.Context
abgebrochen wird, d. h. die Frist. - Done: Gibt einen schreibgeschützten Kanal zurück. Wenn der Context abgebrochen wird oder die Frist erreicht ist, wird dieser Kanal geschlossen, was das Ende der Context-Kette anzeigt. Mehrfache Aufrufe der Methode
Done
geben denselben Kanal zurück. - Err: Gibt den Grund für das Ende des
context.Context
zurück. Er gibt nur dann einen Wert ungleich Null zurück, wenn der vonDone
zurückgegebene Kanal geschlossen ist. Es gibt zwei Fälle für den Rückgabewert:- Wenn der
context.Context
abgebrochen wird, gibt erCanceled
zurück. - Wenn für den
context.Context
ein Timeout auftritt, gibt erDeadlineExceeded
zurück.
- Wenn der
- Value: Ruft den Wert ab, der dem Schlüssel im
context.Context
entspricht, ähnlich derget
-Methode einer Map. Für denselben Kontext geben mehrere Aufrufe vonValue
mit demselben Schlüssel dasselbe Ergebnis zurück. Wenn kein entsprechender Schlüssel vorhanden ist, wirdnil
zurückgegeben. Schlüssel-Wert-Paare werden über die MethodeWithValue
geschrieben.
2. Context erstellen
2.1 Den Root-Context erstellen
Es gibt hauptsächlich zwei Möglichkeiten, den Root-Context zu erstellen:
context.Background() context.TODO()
Analysiert man den Quellcode, gibt es keinen großen Unterschied zwischen context.Background
und context.TODO
. Beide werden verwendet, um den Root-Context zu erstellen, der ein leerer Context ohne jegliche Funktionalität ist. Im Allgemeinen verwenden wir jedoch context.Background
, um einen Root-Context als Start-Context zu erstellen, der nach unten weitergegeben wird, wenn die aktuelle Funktion keinen Context als Eingabeparameter hat.
2.2 Child-Contexte erstellen
Nachdem der Root-Context erstellt wurde, hat er keine Funktionalität. Damit der Context in unseren Programmen nützlich ist, verlassen wir uns auf die von dem Paket context
bereitgestellten Funktionen der With
-Serie zur Ableitung.
Es gibt hauptsächlich die folgenden Ableitungsfunktionen:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
Basierend auf dem aktuellen Context erstellt jede With
-Funktion einen neuen Context. Dies ähnelt der uns bekannten Baumstruktur. Der aktuelle Context wird als Eltern-Context und der neu abgeleitete Context als Kind-Context bezeichnet. Durch den Root-Context können vier Arten von Contexten mithilfe der vier Methoden der With
-Serie abgeleitet werden. Jeder Context kann weiterhin neue Kind-Contexte ableiten, indem er die Methoden der With
-Serie auf die gleiche Weise aufruft, wodurch die gesamte Struktur wie ein Baum aussieht.
3. Wozu dient der Context?
Context hat hauptsächlich zwei Verwendungszwecke, die auch häufig in Projekten verwendet werden:
- Zur gleichzeitigen Steuerung, um Goroutinen ordnungsgemäß zu beenden.
- Zum Übergeben von Context-Informationen.
Im Allgemeinen ist Context ein Mechanismus zum Übergeben von Werten und Senden von Abbruchsignalen zwischen Eltern- und Kind-Goroutinen.
3.1 Gleichzeitige Steuerung
Ein typischer Server läuft kontinuierlich und wartet darauf, Anfragen von Clients oder Browsern zu empfangen und zu beantworten. Betrachten Sie dieses Szenario: In einer Backend-Microservice-Architektur, wenn ein Server eine Anfrage empfängt, wird die Aufgabe bei komplexer Logik nicht in einer einzigen Goroutine erledigt. Stattdessen werden viele Goroutinen erstellt, die zusammenarbeiten, um die Anfrage zu bearbeiten. Nachdem eine Anfrage eingegangen ist, durchläuft sie zuerst einen RPC1-Aufruf, dann RPC2, und dann werden zwei weitere RPCs erstellt und ausgeführt. Es gibt einen weiteren RPC-Aufruf (RPC5) innerhalb von RPC4. Das Ergebnis wird zurückgegeben, nachdem alle RPC-Aufrufe erfolgreich waren. Angenommen, während des gesamten Aufrufprozesses tritt in RPC1 ein Fehler auf. Ohne Context müssten wir warten, bis alle RPCs abgeschlossen sind, bevor wir das Ergebnis zurückgeben, was tatsächlich viel Zeit verschwendet. Denn sobald ein Fehler auftritt, können wir das Ergebnis direkt bei RPC1 zurückgeben, ohne auf den Abschluss der nachfolgenden RPCs zu warten. Wenn wir direkt bei RPC1 einen Fehler zurückgeben und nicht warten, bis die nachfolgenden RPCs fortgesetzt werden, ist die Ausführung der nachfolgenden RPCs tatsächlich bedeutungslos und verschwendet nur Rechen- und E/A-Ressourcen. Nach der Einführung des Context können wir dieses Problem gut lösen. Wenn die Kind-Goroutinen nicht mehr benötigt werden, können wir sie benachrichtigen, sich über den Context ordnungsgemäß zu schließen.
3.1.1 context.WithCancel
Die Methode ist wie folgt definiert:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
Die Funktion context.WithCancel
ist eine Abbruchsteuerungsfunktion. Sie benötigt nur einen Context als Parameter und kann einen neuen Kind-Context und eine Abbruchfunktion CancelFunc
vom context.Context
ableiten. Indem wir diesen Kind-Context an neue Goroutinen übergeben, können wir die Schließung dieser Goroutinen steuern. Sobald wir die zurückgegebene Abbruchfunktion CancelFunc
ausführen, werden der aktuelle Context und seine Kind-Contexte abgebrochen, und alle Goroutinen erhalten synchron das Abbruchsignal.
Anwendungsbeispiel:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // Lasse goroutine1 und goroutine2 6 Sekunden lang laufen fmt.Println("end working!!!") cancel() // Benachrichtige goroutine1 und goroutine2, sich zu schließen time.Sleep(1 * time.Second) } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // Nachdem die Haupt-Goroutine cancel aufgerufen hat, wird ein Signal an den ctx.Done()-Kanal gesendet, und dieser Teil empfängt die Nachricht return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
Laufergebnis:
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
end working!!!
goroutine1 exit!
goroutine2 exit!
ctx, cancel := context.WithCancel(context.Background())
leitet einen ctx
mit einer Rückgabefunktion cancel
ab und übergibt ihn an die Kind-Goroutinen. In den nächsten 6 Sekunden, da die Funktion cancel
nicht ausgeführt wird, führen die Kind-Goroutinen immer die default
-Anweisung aus und geben die Überwachungsinformationen aus. Nach 6 Sekunden wird cancel
aufgerufen. Zu diesem Zeitpunkt empfangen die Kind-Goroutinen eine Nachricht vom Kanal ctx.Done()
und führen return
aus, um zu beenden.
3.1.2 context.WithDeadline
Die Methode ist wie folgt definiert:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context.WithDeadline
ist auch eine Abbruchsteuerungsfunktion. Die Methode hat zwei Parameter. Der erste Parameter ist ein Context und der zweite Parameter ist die Frist. Sie gibt auch einen Kind-Context und eine Abbruchfunktion CancelFunc
zurück. Wenn wir sie verwenden, können wir vor Ablauf der Frist die CancelFunc
manuell aufrufen, um den Kind-Context abzubrechen und den Ausgang der Kind-Goroutinen zu steuern. Wenn wir die CancelFunc
bis zum Ablauf der Frist nicht aufgerufen haben, empfängt der Kanal Done()
des Kind-Context auch ein Abbruchsignal, um den Ausgang der Kind-Goroutinen zu steuern.
Anwendungsbeispiel:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second)) // Setze das Timeout auf 4 Sekunden ab der aktuellen Zeit defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // Lasse goroutine1 und goroutine2 6 Sekunden lang laufen fmt.Println("end working!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // Empfange das Signal nach 4 Sekunden return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
Laufergebnis:
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!
Wir haben die Funktion cancel
nicht aufgerufen, aber nach 4 Sekunden empfing ctx.Done()
in den Kind-Goroutinen das Signal, gab exit
aus und die Kind-Goroutinen wurden beendet. So verwendet man WithDeadline
, um einen Kind-Context abzuleiten.
3.1.3 context.WithTimeout
Die Methode ist definiert als:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
context.WithTimeout
ist in seiner Funktion ähnlich wie context.WithDeadline
. Beide werden verwendet, um den Kind-Context aufgrund eines Timeouts abzubrechen. Der einzige Unterschied besteht im zweiten übergebenen Parameter. Der zweite von context.WithTimeout
übergebene Parameter ist keine bestimmte Zeit, sondern eine Zeitdauer.
Anwendungsbeispiel:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // Lasse goroutine1 und goroutine2 6 Sekunden lang laufen fmt.Println("end working!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // Nachdem die Haupt-Goroutine cancel aufgerufen hat, wird ein Signal an den ctx.Done()-Kanal gesendet, und dieser Teil empfängt die Nachricht return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
Laufergebnis:
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!
Das Programm ist sehr einfach. Es ist im Grunde das gleiche wie der vorherige Beispielcode für context.WithDeadline
, außer dass die Methode zum Ableiten des Context in context.WithTimeout
geändert wurde. Insbesondere ist der zweite Parameter keine bestimmte Zeit mehr, sondern eine bestimmte Zeitdauer von 4 Sekunden. Das Ausführungsergebnis ist ebenfalls das gleiche.
3.1.4 context.WithValue
Die Methode ist definiert als:
func WithValue(parent Context, key, val interface{}) Context
Die Funktion context.WithValue
erstellt einen Kind-Context aus dem Eltern-Context zur Wertübergabe. Die Funktionsparameter sind der Eltern-Context, ein Schlüssel-Wert-Paar (key, val). Sie gibt einen Context zurück. In Projekten wird diese Methode im Allgemeinen zum Übergeben von Context-Informationen verwendet, z. B. der eindeutigen Anfrage-ID und der Trace-ID, für die Link-Verfolgung und die Übergabe von Konfigurationen.
Anwendungsbeispiel:
package main import ( "context" "fmt" "time" ) func func1(ctx context.Context) { fmt.Printf("name is: %s", ctx.Value("name").(string)) } func main() { ctx := context.WithValue(context.Background(), "name", "leapcell") go func1(ctx) time.Sleep(time.Second) }
Laufergebnis:
name is: leapcell
Leapcell: Die beste Serverless-Plattform für Golang-App-Hosting
Abschließend empfehle ich eine Plattform, die sich am besten für die Bereitstellung von Golang-Diensten eignet: Leapcell
1. Multi-Language-Support
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Inaktivitätsgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Einblicke.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf das Erstellen.
Erkunden Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ