Grundlegendes zum Kontextpaket in Golang

Bild


Das Kontextpaket in Go ist nützlich für Interaktionen mit APIs und langsamen Prozessen, insbesondere in Systemen mit Produktionsqualität, die Webanforderungen verarbeiten. Mit ihrer Hilfe können Goroutinen über die Notwendigkeit informiert werden, ihre Arbeit abzuschließen.


Im Folgenden finden Sie eine kleine Anleitung, die Ihnen bei der Verwendung dieses Pakets in Ihren Projekten hilft, sowie einige der Best Practices und Fallstricke.


(Hinweis Lane: Der Kontext wird in vielen Paketen verwendet, z. B. bei der Arbeit mit Docker. )


Bevor Sie anfangen


Um Kontexte verwenden zu können, müssen Sie verstehen, was Goroutine und Kanäle sind. Ich werde versuchen, sie kurz zu betrachten. Wenn Sie bereits mit ihnen vertraut sind, gehen Sie direkt zum Abschnitt Kontext.


Gorutin


Die offizielle Dokumentation besagt, dass "Gorutin ein leichter Strom der Ausführung ist." Goroutinen sind leichter als Threads, daher ist ihre Verwaltung relativ weniger ressourcenintensiv.


Sandkasten


package main import "fmt" // ,   Hello func printHello() { fmt.Println("Hello from printHello") } func main() { //   //       go func(){fmt.Println("Hello inline")}() //     go printHello() fmt.Println("Hello from main") } 

Wenn Sie dieses Programm ausführen, werden Sie sehen, dass nur Hello from main gedruckt wird. Tatsächlich beginnen beide Goroutinen, aber die Hauptenden enden früher. Die Goroutinen brauchen also einen Weg, um main über das Ende ihrer Hinrichtung zu informieren, und damit sie darauf wartet. Hier helfen uns Kanäle.


Kanäle


Kanäle sind eine Art der Kommunikation zwischen Goroutinen. Sie werden verwendet, wenn Sie Ergebnisse, Fehler oder andere Informationen von einer Goroutine auf eine andere übertragen möchten. Es gibt verschiedene Arten von Kanälen, z. B. empfängt ein Kanal vom Typ int Ganzzahlen und ein Kanal vom Typ error empfängt Fehler usw.


Angenommen, wir haben einen Kanal vom Typ int . Wenn Sie etwas an den Kanal senden möchten, lautet die Syntax ch <- 1 . Sie können etwas aus dem Kanal wie folgt erhalten: var := <- ch , d.h. Nehmen Sie den Wert aus dem Kanal und speichern Sie ihn in der Variablen var .


Der folgende Code zeigt, wie Sie mithilfe der Kanäle bestätigen, dass die Goroutinen ihre Arbeit abgeschlossen und ihre Werte an main .


Hinweis: Wartende Gruppen können auch für die Synchronisierung verwendet werden. In diesem Artikel habe ich jedoch Kanäle für Codebeispiele ausgewählt, da wir sie später im Kontextabschnitt verwenden werden.


Sandkasten


 package main import "fmt" //       int   func printHello(ch chan int) { fmt.Println("Hello from printHello") //     ch <- 2 } func main() { //  .       make //       : // ch := make(chan int, 2),       . ch := make(chan int) //  .  ,    . //       go func(){ fmt.Println("Hello inline") //     ch <- 1 }() //     go printHello(ch) fmt.Println("Hello from main") //      //     ,    i := <- ch fmt.Println("Received ",i) //      //    ,      <- ch } 

Kontext


Mit dem Kontextpaket in go können Sie Daten in einer Art „Kontext“ an Ihr Programm übergeben. Der Kontext, wie eine Zeitüberschreitung, eine Frist oder ein Kanal, signalisiert ein Herunterfahren und die Rückgabe von Anrufen.


Wenn Sie beispielsweise eine Webanforderung stellen oder einen Systembefehl ausführen, empfiehlt es sich, eine Zeitüberschreitung für Systeme mit Produktionsqualität zu verwenden. Wenn die API, auf die Sie zugreifen, langsam ist, ist es unwahrscheinlich, dass Sie Anforderungen in Ihrem System akkumulieren möchten, da dies zu einer erhöhten Auslastung und einer verringerten Leistung bei der Verarbeitung Ihrer eigenen Anforderungen führen kann. Das Ergebnis ist ein Kaskadeneffekt.


Und hier kann der Timeout- oder Deadline-Kontext genau richtig sein.


Kontexterstellung


Mit dem Kontextpaket können Sie den Kontext auf folgende Weise erstellen und erben:


context.Background () ctx Kontext


Diese Funktion gibt einen leeren Kontext zurück. Es sollte nur auf einer hohen Ebene verwendet werden (im Haupt- oder im Anforderungshandler der höchsten Ebene). Es kann verwendet werden, um andere Kontexte zu erhalten, die wir später diskutieren werden.


 ctx, cancel := context.Background() 

Hinweis trans.: Es gibt eine Ungenauigkeit im Originalartikel, das richtige Beispiel für die Verwendung von context.Background ist der folgende:


 ctx := context.Background() 

context.TODO () ctx Kontext


Diese Funktion erstellt auch einen leeren Kontext. Und es sollte auch nur auf hoher Ebene verwendet werden, entweder wenn Sie nicht sicher sind, welchen Kontext Sie verwenden sollen, oder wenn die Funktion noch nicht den gewünschten Kontext empfängt. Dies bedeutet, dass Sie (oder jemand, der den Code unterstützt) planen, der Funktion später Kontext hinzuzufügen.


 ctx, cancel := context.TODO() 

Hinweis trans.: Der Originalartikel enthält eine Ungenauigkeit, das richtige Beispiel für die Verwendung von context.TODO wie folgt:


 ctx := context.TODO() 

Interessanterweise werfen Sie einen Blick auf den Code , er ist absolut der gleiche wie der Hintergrund. Der einzige Unterschied besteht darin, dass Sie in diesem Fall die statischen Analysetools verwenden können, um die Gültigkeit der Kontextübertragung zu überprüfen. Dies ist ein wichtiges Detail, da diese Tools dazu beitragen, potenzielle Fehler frühzeitig zu erkennen und in die CI / CD-Pipeline aufgenommen werden können.


Von hier aus :


 var ( background = new(emptyCtx) todo = new(emptyCtx) ) 

context.WithValue (übergeordneter Kontext, Schlüssel, val-Schnittstelle {}) (ctx-Kontext, Abbrechen CancelFunc)


Hinweis Lane: Es gibt eine Ungenauigkeit im Originalartikel, die korrekte Signatur für den context.WithValue wird wie folgt lauten:


 context.WithValue(parent Context, key, val interface{}) Context 

Diese Funktion nimmt einen Kontext und gibt einen daraus abgeleiteten Kontext zurück, in dem der Wert val key und den gesamten Kontextbaum durchläuft. Das heißt, sobald Sie einen WithValue Kontext erstellen, WithValue jeder abgeleitete Kontext diesen Wert.


Es wird nicht empfohlen, kritische Parameter mithilfe von Kontextwerten zu übergeben. Stattdessen sollten Funktionen diese explizit in der Signatur übernehmen.


 ctx := context.WithValue(context.Background(), key, "test") 

context.WithCancel (übergeordneter Kontext) (ctx-Kontext, Abbrechen von CancelFunc)


Hier wird es etwas interessanter. Diese Funktion erstellt einen neuen Kontext aus dem an sie übergebenen übergeordneten Element. Das übergeordnete Element kann der Hintergrundkontext oder der Kontext sein, der als Argument an die Funktion übergeben wird.


Der abgeleitete Kontext und die Rückgängig-Funktion werden zurückgegeben. Nur die Funktion, die es erstellt, sollte die Funktion aufrufen, um den Kontext abzubrechen. Sie können die Rückgängig-Funktion an andere Funktionen übergeben, wenn Sie möchten. Dies wird jedoch dringend empfohlen. In der Regel wird diese Entscheidung aufgrund eines Missverständnisses der Kontextstornierung getroffen. Aus diesem Grund können von diesem übergeordneten Element generierte Kontexte das Programm beeinflussen, was zu einem unerwarteten Ergebnis führt. Kurz gesagt, es ist besser, NIEMALS eine Abbruchfunktion zu übergeben.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Hinweis Lane: Im Originalartikel hat der Autor anscheinend fälschlicherweise den context.WithCancel context.WithDeadline . context.WithCancel gab ein Beispiel mit context.WithDeadline . Das richtige Beispiel für context.WithCancel wäre:


 ctx, cancel := context.WithCancel(context.Background()) 

context.WithDeadline (übergeordneter Kontext, d time.Time) (ctx-Kontext, Abbrechen von CancelFunc)


Diese Funktion gibt einen abgeleiteten Kontext von ihrem übergeordneten Kontext zurück, der nach einer Frist oder einem Aufruf der Abbruchfunktion abgebrochen wird. Sie können beispielsweise einen Kontext erstellen, der zu einem bestimmten Zeitpunkt automatisch abgebrochen und an untergeordnete Funktionen weitergegeben wird. Wenn dieser Kontext nach Ablauf der Frist abgebrochen wird, sollten alle Funktionen, die diesen Kontext haben, durch Benachrichtigung benachrichtigt werden.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

context.WithTimeout (übergeordneter Kontext, timeout time.Duration) (ctx-Kontext, Abbrechen von CancelFunc)


Diese Funktion ähnelt dem Kontext.WithDeadline. Der Unterschied besteht darin, dass die Zeitdauer als Eingabe verwendet wird. Diese Funktion gibt einen abgeleiteten Kontext zurück, der beim Aufruf der Abbruchfunktion oder nach einer bestimmten Zeit abgebrochen wird.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Hinweis Lane: Im Originalartikel hat der Autor anscheinend fälschlicherweise den context.WithTimeout context.WithDeadline . context.WithTimeout gab ein Beispiel mit context.WithDeadline . Das richtige Beispiel für den context.WithTimeout wäre dies:


 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 

Empfang und Verwendung von Kontexten in Ihren Funktionen


Nachdem wir nun wissen, wie Kontexte erstellt werden (Hintergrund und TODO) und wie Kontexte generiert werden (WithValue, WithCancel, Deadline und Timeout), wollen wir diskutieren, wie sie verwendet werden.


Im folgenden Beispiel können Sie sehen, dass die Funktion, die den Kontext verwendet, die Goroutine startet und erwartet, dass sie den Kontext zurückgibt oder abbricht. Die select-Anweisung hilft uns zu bestimmen, was zuerst passiert, und die Funktion zu beenden.


Nach dem Schließen des Kanals Done <-ctx.Done() wird der Fall case <-ctx.Done(): ausgewählt. Sobald dies geschieht, sollte die Funktion die Arbeit unterbrechen und sich auf eine Rückkehr vorbereiten. Dies bedeutet, dass Sie alle offenen Verbindungen schließen, Ressourcen freigeben und von der Funktion zurückkehren müssen. Es gibt Zeiten, in denen die Freigabe von Ressourcen die Rückgabe verzögern kann, z. B. hängt die Bereinigung. Sie müssen dies berücksichtigen.


Das Beispiel, das diesem Abschnitt folgt, ist ein vollständig abgeschlossenes Go-Programm, das Zeitüberschreitungen und Rückgängig-Funktionen veranschaulicht.


 // ,  -      // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } 

Beispiel


Wie wir gesehen haben, können Sie mithilfe von Kontexten mit Fristen und Zeitüberschreitungen arbeiten und auch die Abbruchfunktion aufrufen, um allen Funktionen mithilfe eines abgeleiteten Kontexts klar zu machen, dass Sie Ihre Arbeit abschließen und die Rückgabe ausführen müssen. Betrachten Sie ein Beispiel:


Hauptfunktion:


  • Erstellt einen Abbruchfunktionskontext
  • Ruft die Abbruchfunktion nach einer beliebigen Zeitüberschreitung auf

doWorkContext Funktion:


  • Erstellt einen abgeleiteten Kontext mit einer Zeitüberschreitung
  • Dieser Kontext wird abgebrochen, wenn die Hauptfunktion cancelFunction aufruft, das Timeout abläuft oder doWorkContext seine cancelFunction aufruft.
  • Führt Goroutine aus, um eine langsame Aufgabe auszuführen, wobei der resultierende Kontext übergeben wird
  • Wartet darauf, dass die Goroutinen abgeschlossen sind oder der Kontext von main gelöscht wird, je nachdem, was zuerst eintritt

sleepRandomContext Funktion:


  • Startet Goroutine, um eine langsame Aufgabe auszuführen
  • Wartet, bis die Goroutine fertig ist, oder
  • Wartet darauf, dass der Kontext von der Hauptfunktion abgebrochen wird, Timeout oder ruft seine eigene cancelFunction auf

sleepRandom Funktion:


  • Schläft zufällig ein

In diesem Beispiel wird der Ruhemodus verwendet, um eine zufällige Verarbeitungszeit zu simulieren. In der Realität können Sie jedoch Kanäle verwenden, um diese Funktion über den Beginn der Reinigung zu signalisieren und auf die Bestätigung des Kanals zu warten, dass die Reinigung abgeschlossen ist.


Sandbox (Es sieht so aus, als ob die zufällige Zeit, die ich in der Sandbox verwende, praktisch unverändert ist. Versuchen Sie dies auf Ihrem lokalen Computer, um die Zufälligkeit zu sehen.)


Github


 package main import ( "context" "fmt" "math/rand" "Time" ) //   func sleepRandom(fromFunction string, ch chan int) { //    defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //    //   , // «»      seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") //   ,     if ch != nil { ch <- sleeptime } } // ,       // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,    doWorkContext  // doWorkContext  main  cancelFunction //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } //  ,         //       //   ,      main func doWorkContext(ctx context.Context) { //          - //  150  //  ,   ,   150  ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //         defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() //       //         , //      ,    ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //  select      select { case <-ctx.Done(): //   ,           //     ,   main   cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //   ,       fmt.Println("sleepRandomContext returned") } } func main() { //   background ctx := context.Background() //     ctxWithCancel, cancelFunction := context.WithCancel(ctx) //      //        defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() //     - //   ,        go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //   doWorkContext(ctxWithCancel) } 

Fallstricke


Wenn die Funktion den Kontext verwendet, stellen Sie sicher, dass Stornierungsbenachrichtigungen ordnungsgemäß behandelt werden. Beispielsweise schließt exec.CommandContext nicht, bis der Befehl alle vom Prozess ( Github ) erstellten Gabeln abgeschlossen hat, d. H., Dass das Abbrechen des Kontexts nicht sofort von der Funktion zurückkehrt, wenn Sie mit cmd.Wait () warten. bis alle Gabeln des externen Befehls die Verarbeitung abgeschlossen haben.


Wenn Sie ein Timeout oder eine Frist mit einer maximalen Laufzeit verwenden, funktioniert dies möglicherweise nicht wie erwartet. In solchen Fällen ist es besser, Timeouts mithilfe von time.After zu implementieren.


Best Practices


  1. context.Background sollte nur auf der höchsten Ebene als Wurzel aller abgeleiteten Kontexte verwendet werden.
  2. context.TODO sollte verwendet werden, wenn Sie nicht sicher sind, was Sie verwenden sollen, oder wenn die aktuelle Funktion in Zukunft den Kontext verwenden wird.
  3. Es wird empfohlen, den Kontext zu löschen. Das Löschen und Beenden dieser Funktionen kann jedoch einige Zeit dauern.
  4. context.Value sollte so sparsam wie möglich verwendet werden und nicht zum Übergeben optionaler Parameter verwendet werden. Dies macht die API unverständlich und kann zu Fehlern führen. Solche Werte sollten als Argumente übergeben werden.
  5. Speichern Sie Kontexte nicht in einer Struktur, sondern übergeben Sie sie explizit in Funktionen, vorzugsweise als erstes Argument.
  6. Übergeben Sie niemals einen Null-Kontext als Argument. Verwenden Sie im Zweifelsfall TODO.
  7. Die Context verfügt nicht über eine Abbruchmethode, da nur die Funktion, die den Kontext erzeugt, ihn abbrechen sollte.

Vom Übersetzer


In unserem Unternehmen verwenden wir das Context-Paket aktiv bei der Entwicklung von Serveranwendungen für den internen Gebrauch. Solche Anwendungen für das normale Funktionieren erfordern jedoch zusätzlich zum Kontext zusätzliche Elemente, wie z.


  • Protokollierung
  • Signalverarbeitung zum Beenden, Neuladen und Protokollieren der Anwendung
  • Arbeiten Sie mit PID-Dateien
  • Arbeiten Sie mit Konfigurationsdateien
  • Usw

Aus diesem Grund haben wir uns irgendwann entschlossen, alle unsere Erfahrungen zusammenzufassen, und Hilfspakete erstellt, die das Schreiben von Anwendungen (insbesondere Anwendungen mit APIs) erheblich vereinfachen. Wir haben unsere Entwicklungen öffentlich zugänglich gemacht und jeder kann sie nutzen. Im Folgenden finden Sie einige Links zu Paketen, die zur Lösung solcher Probleme hilfreich sind:



Lesen Sie auch andere Artikel in unserem Blog:


Source: https://habr.com/ru/post/de461723/


All Articles