Kontekstbasert programmering i Go

Go programmer som kjører flere samtidige beregninger i goroutines trenger å styre livet deres. Runaway goroutines kan komme inn i uendelige løkker, hindre andre ventende goroutiner, eller bare ta for lang tid. Ideelt sett bør du kunne avbryte goroutiner eller få dem til å gå ut etter en mote. 

Skriv inn innholdsbasert programmering. Go 1.7 introduserte kontekstpakken, som gir nøyaktig disse evnene, samt evnen til å knytte vilkårlig verdier med en kontekst som beveger seg med utførelsen av forespørsler og tillater utgående kommunikasjon og informasjon som passerer. 

I denne opplæringen lærer du innspillene i sammenhenger i Go, når og hvordan du bruker dem, og hvordan du unngår å misbruke dem. 

Hvem trenger en kontekst?

Konteksten er en veldig nyttig abstraksjon. Den lar deg inkapslere informasjon som ikke er relevant for kjerneberegningen som forespørsels-ID, autorisasjons token og timeout. Det er flere fordeler med denne innkapslingen:

  • Det skiller kjernekalkylparametrene fra driftsparametrene.
  • Den kodifiserer felles operative aspekter og hvordan man kommuniserer dem over grenser.
  • Den gir en standardmekanisme for å legge til informasjon utenom bånd uten å endre signaturer.

Kontekstgrensesnittet

Her er hele kontekstgrensesnittet:

skriv inn kontekstgrensesnitt Deadline () (deadline time.Time, ok bool) Done () <-chan struct Err() error Value(key interface) interface

Følgende avsnitt forklarer formålet med hver metode.

Tidsfristen () Metode

Fristen returnerer tiden når arbeid utført på vegne av denne sammenhengen skal avlyses. Frist for retur ok == false når ingen tidsfrist er angitt. Etterfølgende samtaler til tidsfrist gir de samme resultatene.

Ferdig () -metoden

Ferdig () returnerer en kanal som er stengt når arbeid utført på vegne av denne konteksten skal avbrytes. Ferdig kan returnere null hvis denne konteksten aldri kan kanselleres. Etterfølgende samtaler til Ferdig () returnerer samme verdi.

  • Konteksten.WithCancel () -funksjonen sørger for at Ferdig kanal lukkes når avbrudd blir ringt. 
  • Konteksten.WithDeadline () -funksjonen ordner at Ferdig kanal lukkes når fristen utløper.
  • Konteksten.WithTimeout () -funksjonen ordner at Ferdig kanal lukkes når tidsgrensen går.

Ferdig kan brukes i utvalgte setninger:

 // Stream genererer verdier med DoSomething og sender dem // til ut til DoSomething returnerer en feil eller ctx.Done er // lukket. func Stream (ctx context.Context, ut chan<- Value) error  for  v, err := DoSomething(ctx) if err != nil  return err  select  case <-ctx.Done(): return ctx.Err() case out <- v:   

Se denne artikkelen fra Go-bloggen for flere eksempler på hvordan du bruker en ferdig kanal for avbestilling.

Err () Metoden

Err () returnerer null så lenge ferdig kanal er åpen. Det kommer tilbake avbrutt hvis konteksten ble kansellert eller DeadlineExceeded hvis kontekstens frist gikk eller tidsavbrudd utløpt. Etter at Klar er lukket, returnerer påfølgende samtaler til Err () samme verdi. Her er definisjonene:

// Avbryt er feilen returnert av Context.Err når // konteksten er avbrutt. var Canceled = errors.New ("kontekst avbrutt") // DeadlineExceeded er feilen returnert av Context.Err // når kontekstens frist går. var DeadlineExceeded error = deadlineExceededError  

Verdien () Metode

Verdi returnerer verdien som er knyttet til denne konteksten for en nøkkel, eller null hvis ingen verdi er knyttet til nøkkelen. Etterfølgende samtaler til Verdi med samme nøkkel returnerer det samme resultatet.

Bruk kun kontekstverdier for forespørselsskannede data som overfører prosesser og API-grenser, ikke for å overføre valgfrie parametere til funksjoner.

En nøkkel identifiserer en bestemt verdi i en kontekst. Funksjoner som ønsker å lagre verdier i Kontekst, tilordner vanligvis en nøkkel i en global variabel og bruker den nøkkelen som argumentet til context.WithValue () og Context.Value (). En nøkkel kan være hvilken som helst type som støtter likestilling.

Kontekstomfang

Kontekster har rekkevidder. Du kan oppnå omfang fra andre områder, og foreldreområdet har ingen tilgang til verdier i avledede omfang, men avledede omfang har tilgang til foreldrenes omfangsverdier. 

Konteksten danner et hierarki. Du starter med context.Background () eller context.TODO (). Når du ringer WithCancel (), WithDeadline () eller WithTimeout (), oppretter du en avledet kontekst og mottar en kanselleringsfunksjon. Det viktigste er at når en overordnet kontekst er kansellert eller utløpt, er alle dens avledede sammenhenger.

Du bør bruke context.Background () i funksjonene main (), init () og tester. Du bør bruke context.TODO () hvis du ikke er sikker på hvilken kontekst du vil bruke.

Legg merke til at Bakgrunn og TODO er ikke avbrytes.

Frister, tidsavbrudd og avbestillinger

Som du husker, WithDeadline () og WithTimeout () returkontekster som blir kansellert automatisk, mens WithCancel () returnerer en kontekst og må avbrytes eksplisitt. Alle av dem returnerer en kanselleringsfunksjon, så selv om timeout / deadline ikke utløper ennå, kan du likevel avbryte en avledet kontekst. 

La oss undersøke et eksempel. Først, her er contextDemo () -funksjonen med et navn og en kontekst. Den går i en uendelig sløyfe, skriver ut til konsollen sitt navn og dens konteksts frist hvis det er noe. Så sover det bare et sekund.

pakke main import ("fmt" "kontekst" "tid") func contextDemo (navnestreng, ctx context.Context) for hvis ok fmt.Println (navn, "utløper på:", frist) else fmt .Println (navn, "har ingen tidsfrist") time.Sleep (time.Second)

Hovedfunksjonen skaper tre sammenhenger: 

  • timeoutContext med en tre-sekunders timeout
  • en ikke-utløser avbrytContext
  • deadlineContext, som er avledet fra cancelContext, med en frist på fire timer fra nå

Deretter lanserer den contextDemo funksjonen som tre goroutiner. Alle løper samtidig og skriver ut meldingen hvert sekund. 

Hovedfunksjonen venter deretter på goroutinen med timeoutCancel å bli kansellert ved å lese fra sin ferdige () kanal (vil blokkere til den er stengt). Når tidsavbrudd utløper etter tre sekunder, kalles main () (cancel) Func () som kansellerer goroutinen med cancelContext samt den siste goroutin med den avledede fire timers tidsrammekontekst.

func main () timeout: = 3 * time.Second deadline: = time.Now (). Legg til (4 * time.Hour) timeOutContext, _: = context.WithTimeout (context.Background (), timeout) cancelContext, cancelFunc : = context.WithCancel (context.Background ()) deadlineContext, _: = context.WithDeadline (cancelContext, deadline) gå contextDemo ("[timeoutContext]", timeOutContext) gå contextDemo ("[cancelContext]", cancelContext) go contextDemo "[deadlineContext]", deadlineContext) // Vent på timeout å utløpe <- 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… ")  

Her er utgangen:

[CancelContext] har ingen tidsfrist [deadlineContext] utløper ved: 2017-07-29 09: 06: 02.34260363 [timeoutContext] utløper ved: 2017-07-29 05: 06: 05.342603759 [cancelContext] har ingen deadline [timeoutContext] vil utløper ved: 2017-07-29 05: 06: 05.342603759 [deadlineContext] utløper ved: 2017-07-29 09: 06: 02.34260363 [cancelContext] har ingen deadline [timeoutContext] utløper ved: 2017-07-29 05: 06: 05.342603759 [deadlineContext] utløper ved: 2017-07-29 09: 06: 02.34260363 Avbryter avbryt konteksten ... Avbryt konteksten er blitt kansellert ... Konfristen for tidsfrist har blitt kansellert ... 

Passerer verdier i konteksten

Du kan legge til verdier i en kontekst ved hjelp av funksjonen WithValue (). Legg merke til at den opprinnelige konteksten returneres, ikke en avledet kontekst. Du kan lese verdiene fra konteksten ved hjelp av metoden Value () (). La oss endre vår demo-funksjon for å få navnet sitt fra konteksten i stedet for å sende det som en parameter:

func contextDemo (ctx context.Context) deadline, ok: = ctx.Deadline () navn: = ctx.Value ("navn") for hvis ok fmt.Println (navn, "utløper på:", deadline)  ellers fmt.Println (navn, "har ingen tidsfrist") time.Sleep (time.Second) 

Og la oss endre hovedfunksjonen for å feste navnet via WithValue ():

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

Produksjonen forblir den samme. Se avsnittet om beste praksis for noen retningslinjer om bruk av kontekstverdier på riktig måte.

Beste praksis

Flere gode praksiser har dukket opp rundt kontekstverdier:

  • Unngå å overføre funksjonsargumenter i kontekstverdier.
  • Funksjoner som ønsker å lagre verdier i Kontekst, tilordner vanligvis en nøkkel i en global variabel.
  • Pakker bør definere nøkler som en ikke-eksportert type for å unngå kollisjoner.
  • Pakker som definerer en kontekstnøkkel, skal gi type sikker tilgang til verdiene som er lagret ved hjelp av den aktuelle nøkkelen.

HTTP-forespørselen Kontekst

En av de mest nyttige bruksområdene for sammenhenger er å sende informasjon sammen med en HTTP-forespørsel. Denne informasjonen kan inneholde en forespørsel id, autentiserings legitimasjon og mer. I Go 1.7 tok standard nettverks- / http-pakken fordel av at kontekstpakken ble "standardisert" og lagt til sammenhengsstøtte direkte til forespørselsobjektet:

func (r * Request) Kontekst () context.Context func (r * Request) WithContext (ctx context.Context) * Forespørsel 

Nå er det mulig å legge ved en forespørsel id fra topptekstene helt til sluttbehandleren på en standard måte. Funksjonen WithRequestID () håndterer en forespørsels ID fra "X-Request-ID" header og genererer en ny kontekst med forespørsel id fra en eksisterende kontekst som den bruker. Den overfører den til neste handler i kjeden. Den offentlige funksjonen GetRequestID () gir tilgang til håndtere som kan defineres i andre pakker.

const requestIDKey int = 0 func WithRequestID (neste http.Handler) http.Handler return http.HandlerFunc (func (rw http.ResponseWriter, req * http.Request) // Utdrag forespørsels ID fra forespørselsoverskrift reqID: = req.Header .Get ("X-Request-ID") // Opprett ny kontekst fra forespørsel sammenheng med // forespørselen ID ctx: = context.WithValue (req.Context (), requestIDKey, reqID) // Opprett ny forespørsel med den nye kontekst req = req.WithContext (ctx) // La neste handler i kjeden ta over. next.ServeHTTP (rw, req)) func GetRequestID (ctx context.Context) streng ctx.Value (requestIDKey). streng) func Handle (rw http.ResponseWriter, req * http.Request) reqID: = GetRequestID (req.Context ()) ... func main () handler: = WithRequestID (http.HandlerFunc (Handle)) http. ListenAndServe ("/", handler) 

Konklusjon

Kontekstbasert programmering gir en standard og godt støttet måte å løse to vanlige problemer på: håndtering av goroutinernes levetid og overføring av utgående båndinformasjon over en funksjonskjede. 

Følg de beste praksis og bruk sammenhenger i riktig sammenheng (se hva jeg gjorde der?) Og koden din vil bli betydelig forbedret.