Context-gebaseerd programmeren in Go

Go-programma's die meerdere gelijktijdige berekeningen in goroutines uitvoeren, moeten hun leven beheren. Runaway-gorotrines kunnen in oneindige lussen terechtkomen, andere wachtende goroutines blokkeren of gewoon te lang duren. Idealiter zou je in staat moeten zijn goroutines te annuleren of ze na een mode time-out te hebben. 

Voer inhoud-gebaseerd programmeren in. Go 1.7 introduceerde het contextpakket, dat precies die mogelijkheden biedt, evenals de mogelijkheid om arbitraire waarden te koppelen aan een context die met de uitvoering van verzoeken omgaat en die out-of-band communicatie en informatieoverdracht mogelijk maakt. 

In deze tutorial leer je de details van contexten in Go, wanneer en hoe ze te gebruiken en hoe je ze kunt vermijden om ze te misbruiken.. 

Wie heeft een context nodig?

De context is een zeer nuttige abstractie. Hiermee kunt u informatie inkapselen die niet relevant is voor de kernberekening zoals aanvraag-id, autorisatie-token en time-out. Er zijn verschillende voordelen van deze inkapseling:

  • Het scheidt de belangrijkste berekeningsparameters van de operationele parameters.
  • Het codificeert algemene operationele aspecten en hoe deze over grenzen heen te communiceren.
  • Het biedt een standaardmechanisme om out-of-band informatie toe te voegen zonder ondertekeningen te veranderen.

De contextinterface

Dit is de volledige contextinterface:

type Context-interface Deadline () (deadline time.Time, ok bool) Gereed () <-chan struct Err() error Value(key interface) interface

In de volgende secties wordt het doel van elke methode uitgelegd.

De methode Deadline ()

Deadline geeft het tijdstip waarop werk gedaan in naam van deze context moet worden geannuleerd. Deadline komt terug ok == false wanneer er geen deadline is gesteld. Opeenvolgende oproepen naar Deadline retourneren dezelfde resultaten.

De gereed () methode

Met Gereed () wordt een kanaal geretourneerd dat wordt gesloten wanneer het werk dat in opdracht van deze context wordt gedaan, moet worden geannuleerd. Klaar kan terug worden geretourneerd als deze context nooit kan worden geannuleerd. Opeenvolgende oproepen naar Gereed () retourneren dezelfde waarde.

  • De context.WithCancel () -functie zorgt ervoor dat het voltooide kanaal wordt gesloten wanneer annuleren wordt aangeroepen. 
  • De functie context.WithDeadline () zorgt ervoor dat het voltooide kanaal wordt gesloten wanneer de deadline verloopt.
  • De context.WithTimeout () -functie zorgt ervoor dat het voltooide kanaal wordt gesloten als de time-out is verstreken.

Klaar kan worden gebruikt in geselecteerde uitspraken:

 // Stream genereert waarden met DoSomething en stuurt ze // uit totdat DoSomething een fout of ctx retourneert.Done is // gesloten. func Stream (ctx context.Context, out chan<- Value) error  for  v, err := DoSomething(ctx) if err != nil  return err  select  case <-ctx.Done(): return ctx.Err() case out <- v:   

Bekijk dit artikel van de Go-blog voor meer voorbeelden van het gebruik van een Done-kanaal voor annulering.

De methode Err ()

Err () retourneert nul zolang het Gedaan kanaal open is. Het komt terug Geannuleerd als de context is geannuleerd of DeadlineExceeded als de deadline van de context is verstreken of de time-out is verstreken. Nadat Klaar is gesloten, geven opeenvolgende aanroepen van Err () dezelfde waarde terug. Hier zijn de definities:

// Geannuleerd is de fout die wordt geretourneerd door Context.Err wanneer de // -context wordt geannuleerd. var Cancelled = errors.New ("context cancelled") // DeadlineExceeded is de fout geretourneerd door Context.Err // wanneer de deadline van de context verstrijkt. var DeadlineExceeded error = deadlineExceededError  

De methode Value ()

Waarde retourneert de waarde die aan deze context is gekoppeld voor een sleutel, of nul als geen waarde aan de sleutel is gekoppeld. Opeenvolgende oproepen naar Value met dezelfde sleutel retourneren hetzelfde resultaat.

Gebruik contextwaarden alleen voor gegevens met request-scopedie die processen en API-grenzen overbrengen, niet voor het doorgeven van optionele parameters aan functies.

Een sleutel identificeert een specifieke waarde in een context. Functies die waarden in Context willen opslaan, wijzen gewoonlijk een sleutel toe aan een globale variabele en gebruiken die sleutel als het argument voor context. With Value () en Context.Value (). Een sleutel kan elk type zijn dat gelijkheid ondersteunt.

Context Scope

Contexten hebben scopes. U kunt scopes berekenen van andere scopes en de bovenliggende scope heeft geen toegang tot waarden in afgeleide scopes, maar afgeleide scopes hebben toegang tot scopes van de bovenliggende waarden. 

De contexten vormen een hiërarchie. U begint met context.Background () of context.TODO (). Telkens wanneer u WithCancel (), WithDeadline () of WithTimeout () aanroept, maakt u een afgeleide context en ontvangt u een annuleringsfunctie. Het belangrijkste is dat wanneer een bovenliggende context geannuleerd of vervallen is, alle daarvan afgeleide contexten zijn.

U moet context.Background () gebruiken in de functies main (), init () en tests. U moet context.TODO () gebruiken als u niet zeker weet welke context u moet gebruiken.

Merk op dat Achtergrond en TOO zijn niet opzegbare.

Deadlines, time-outs en annuleringen

Zoals je je herinnert, retourneren WithDeadline () en WithTimeout () contexten die automatisch worden geannuleerd, terwijl WithCancel () een context retourneert en expliciet moet worden geannuleerd. Ze retourneren allemaal een annuleerfunctie, dus zelfs als de time-out / deadline nog niet is verlopen, kunt u nog steeds elke afgeleide context annuleren. 

Laten we een voorbeeld bekijken. Ten eerste is hier de contextDemo () -functie met een naam en een context. Het loopt in een oneindige lus en drukt zijn naam af op de console en de eventuele deadline van de context. Dan slaapt het gewoon een seconde.

pakket main import ("fmt" "context" "time") func contextDemo (naamreeks, ctx context.Context) for if ok fmt.Println (naam, "verloopt op:", deadline) else fmt .Println (naam, "heeft geen deadline") time.Sleep (time.Second)

De hoofdfunctie creëert drie contexten: 

  • timeoutContext met een time-out van drie seconden
  • een niet-aflopende cancelContext
  • deadlineContext, afgeleid van cancelContext, met een deadline over vier uur

Vervolgens wordt de contextDemo-functie gestart als drie goroutines. Alle werken gelijktijdig en drukken hun bericht elke seconde af. 

De hoofdfunctie wacht vervolgens tot de goroutine met de time-out annuleren om te worden geannuleerd door te lezen vanuit het Done () -kanaal (blokkeert totdat deze gesloten is). Nadat de time-out na drie seconden is verlopen, roept main () de cancelFunc () aan die de goroutine annuleert met de cancelContext en de laatste goroutine met de afgeleide vier uur deadline-context.

func main () timeout: = 3 * time.Second deadline: = time.Now (). Add (4 * time.Hour) timeOutContext, _: = context.WithTimeout (context.Background (), timeout) cancelContext, cancelFunc : = context.WithCancel (context.Background ()) deadlineContext, _: = context.WithDeadline (cancelContext, deadline) go contextDemo ("[timeoutContext]" timeOutContext) go context doemo ("[cancelContext]", cancelContext) go contextDemo ( "[deadlineContext]", deadlineContext) // Wacht tot de time-out verloopt <- timeOutContext.Done() // This will cancel the deadline context as well as its // child - the cancelContext fmt.Println("Cancelling the cancel context… ") cancelFunc() <- cancelContext.Done() fmt.Println("The cancel context has been cancelled… ") // Wait for both contexts to be cancelled <- deadlineContext.Done() fmt.Println("The deadline context has been cancelled… ")  

Dit is de uitvoer:

[cancelContext] heeft geen deadline [deadlineContext] verloopt op: 2017-07-29 09: 06: 02.34260363 [timeoutContext] verloopt op: 2017-07-29 05: 06: 05.342603759 [cancelContext] heeft geen deadline [timeoutContext] zal vervalt op: 2017-07-29 05: 06: 05.342603759 [deadlineContext] verloopt op: 2017-07-29 09: 06: 02.34260363 [cancelContext] heeft geen deadline [timeoutContext] verstrijkt op: 2017-07-29 05: 06: 05.342603759 [deadlineContext] verloopt op: 2017-07-29 09: 06: 02.34260363 Annuleren van de annuleercontext ... De annuleercontext is geannuleerd ... De deadlinecontext is geannuleerd ... 

Waarden in de context doorgeven

U kunt waarden aan een context koppelen met de functie WithValue (). Merk op dat de oorspronkelijke context wordt geretourneerd, niet een afgeleide context. U kunt de waarden uit de context lezen met de methode Value (). Laten we onze demofunctie aanpassen om de naam uit de context te halen in plaats van deze als een parameter door te geven:

func contextDemo (ctx context.Context) deadline, ok: = ctx.Deadline () naam: = ctx.Value ("naam") voor if ok fmt.Println (naam, "verloopt op:", deadline)  else fmt.Println (naam, "heeft geen deadline") time.Sleep (time.Second) 

En laten we de hoofdfunctie wijzigen om de naam toe te voegen via WithValue ():

go contextDemo (context.WithValue (timeOutContext, "name", "[timeoutContext]")) go contextDemo (context.WithValue (cancelContext, "name", "[cancelContext]")) go contextDemo (context.WithValue (deadlineContext, " name "," [deadlineContext] ")) 

De output blijft hetzelfde. Raadpleeg de sectie met aanbevolen procedures voor enkele richtlijnen voor het correct gebruiken van contextwaarden.

Beste praktijken

Verschillende best practices zijn naar voren gekomen rond contextwaarden:

  • Vermijd functieargumenten door te geven in contextwaarden.
  • Functies die waarden in Context willen opslaan, wijzen doorgaans een sleutel toe aan een globale variabele.
  • Pakketten moeten sleutels definiëren als een niet-geëxporteerd type om botsingen te voorkomen.
  • Pakketten die een Context-sleutel definiëren, moeten typeveilige toegang bieden voor de waarden die met die sleutel zijn opgeslagen.

De HTTP-verzoekcontext

Een van de handigste use cases voor contexten is het doorgeven van informatie samen met een HTTP-verzoek. Die informatie kan een aanvraag-id, authenticatiegegevens en meer bevatten. In Go 1.7 maakte het standaard net / http-pakket gebruik van het contextpakket dat "gestandaardiseerd" kreeg en voegde contextondersteuning rechtstreeks toe aan het verzoekobject:

func (r * Request) Context () context.Context func (r * Request) WithContext (ctx context.Context) * Verzoek 

Het is nu mogelijk om een ​​aanvraag-ID van de kopteksten tot en met de laatste handler op een standaardmanier toe te voegen. De WithRequestID () -handlerfunctie haalt een aanvraag-ID uit de kop "X-Request-ID" en genereert een nieuwe context met de aanvraag-ID uit een bestaande context die wordt gebruikt. Vervolgens wordt het doorgegeven aan de volgende handler in de keten. De openbare functie GetRequestID () biedt toegang tot handlers die in andere pakketten kunnen worden gedefinieerd.

const requestIDKey int = 0 func WithRequestID (volgende http.Handler) http.Handler return http.HandlerFunc (func (rw http.ResponseWriter, req * http.Request) // Extract-aanvraag-ID uit aanvraag header reqID: = req.Header .Get ("X-Request-ID") // Maak een nieuwe context uit de aanvraagcontext met // de aanvraag-ID ctx: = context.WithValue (req.Context (), requestIDKey, reqID) // Maak een nieuw verzoek met de nieuwe context req = req.WithContext (ctx) // Laat de volgende handler in de keten het overnemen next.ServeHTTP (rw, req)) func GetRequestID (ctx context.Context) string ctx.Value (requestIDKey). ( string) func Handle (rw http.ResponseWriter, req * http.Request) reqID: = GetRequestID (req.Context ()) ... func main () handler: = WithRequestID (http.HandlerFunc (Handle)) http. ListenAndServe ("/", handler) 

Conclusie

Context-gebaseerd programmeren biedt een standaard en goed ondersteunde manier om twee veel voorkomende problemen aan te pakken: het beheer van de levensduur van goroutines en het doorgeven van out-of-band informatie over een reeks functies. 

Volg de best practices en gebruik contexten in de juiste context (zie wat ik daar heb gedaan?) En uw code zal aanzienlijk verbeteren.