Programmazione basata sul contesto in Go

I programmi che eseguono più calcoli simultanei nelle goroutine devono gestirne la durata. Le goroutines in fuga possono entrare in loop infiniti, mettere in stallo altre goroutine in attesa o semplicemente impiegare troppo tempo. Idealmente, dovresti essere in grado di cancellare le goroutine o farle scadere dopo una moda. 

Inserisci la programmazione basata sul contenuto. Go 1.7 ha introdotto il pacchetto di contesto, che fornisce esattamente quelle funzionalità e la possibilità di associare valori arbitrari a un contesto che viaggia con l'esecuzione di richieste e consente la comunicazione fuori banda e il passaggio di informazioni. 

In questo tutorial, imparerai i dettagli dei contesti in Go, quando e come usarli, e come evitare di abusarli. 

Chi ha bisogno di un contesto?

Il contesto è un'astrazione molto utile. Ti consente di incapsulare informazioni che non sono rilevanti per il calcolo di base come ID richiesta, token di autorizzazione e timeout. Ci sono molti vantaggi di questo incapsulamento:

  • Separa i parametri di calcolo principali dai parametri operativi.
  • Codifica gli aspetti operativi comuni e come comunicarli attraverso i confini.
  • Fornisce un meccanismo standard per aggiungere informazioni fuori banda senza modificare le firme.

L'interfaccia di contesto

Ecco l'intera interfaccia di contesto:

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

Le seguenti sezioni spiegano lo scopo di ciascun metodo.

Il metodo Deadline ()

La scadenza restituisce il momento in cui il lavoro svolto a nome di questo contesto deve essere annullato. Termine di reso ok == false quando non è impostata alcuna scadenza. Le chiamate successive alla scadenza restituiscono gli stessi risultati.

Il metodo Done ()

Fine () restituisce un canale che viene chiuso quando il lavoro svolto a nome di questo contesto deve essere annullato. Fatto può restituire nil se questo contesto non può mai essere annullato. Le chiamate successive a Done () restituiscono lo stesso valore.

  • La funzione context.WithCancel () consente di chiudere il canale Done quando viene chiamato cancel. 
  • La funzione context.WithDeadline () consente di chiudere il canale Done quando scade la scadenza.
  • La funzione context.WithTimeout () consente di chiudere il canale Done al termine del timeout.

Fatto può essere utilizzato in istruzioni selezionate:

 // Stream genera valori con DoSomething e li invia // a out finché DoSomething non restituisce un errore o ctx.Done è // closed. 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:   

Vedi questo articolo dal blog Go per ulteriori esempi su come utilizzare un canale Done per la cancellazione.

Il metodo Err ()

Err () restituisce nil finché il canale Done è aperto. Ritorna Annullato se il contesto è stato cancellato o DeadlineExceeded se la scadenza del contesto è scaduta o il timeout è scaduto. Dopo aver terminato Done, le successive chiamate a Err () restituiscono lo stesso valore. Ecco le definizioni:

// Cancellato è l'errore restituito da Context.Err quando il // contesto viene cancellato. var Cancelled = errors.New ("context deleted") // DeadlineExceeded è l'errore restituito da Context.Err // quando la scadenza del contesto passa. var DeadlineExceeded error = deadlineExceededError  

Il metodo Value ()

Value restituisce il valore associato a questo contesto per una chiave, o zero se nessun valore è associato alla chiave. Le chiamate successive a Valore con la stessa chiave restituiscono lo stesso risultato.

Utilizzare i valori di contesto solo per i dati con ambito di richiesta che gestiscono i processi ei limiti dell'API, non per passare i parametri facoltativi alle funzioni.

Una chiave identifica un valore specifico in un contesto. Le funzioni che desiderano memorizzare i valori nel contesto in genere allocano una chiave in una variabile globale e la utilizzano come argomento di context.WithValue () e Context.Value (). Una chiave può essere di qualsiasi tipo che supporti l'uguaglianza.

Ambito del contesto

I contesti hanno ambiti. È possibile derivare ambiti da altri ambiti e l'ambito principale non ha accesso ai valori negli ambiti derivati, ma gli ambiti derivati ​​hanno accesso ai valori degli ambiti del genitore. 

I contesti formano una gerarchia. Si inizia con context.Background () o context.TODO (). Ogni volta che chiami WithCancel (), WithDeadline () o WithTimeout (), crei un contesto derivato e ricevi una funzione cancel. L'importante è che quando un contesto genitore viene cancellato o scaduto, tutti i suoi contesti derivati.

Dovresti usare context.Background () nelle funzioni main (), init () e tests. Dovresti usare context.TODO () se non sei sicuro di quale contesto usare.

Si noti che lo sfondo e TODO sono non cancellabile.

Scadenze, scadenze e cancellazioni

Come ricordi, WithDeadline () e WithTimeout () restituiscono i contesti che vengono cancellati automaticamente, mentre WithCancel () restituisce un contesto e deve essere cancellato esplicitamente. Tutti loro restituiscono una funzione di annullamento, quindi anche se il timeout / scadenza non ha ancora scadenza, è comunque possibile annullare qualsiasi contesto derivato. 

Esaminiamo un esempio. Innanzitutto, ecco la funzione contextDemo () con un nome e un contesto. Funziona in un ciclo infinito, stampando sulla console il suo nome e la scadenza del suo contesto, se presente. Quindi dorme solo per un secondo.

pacchetto main import ("fmt" "context" "time") func contextDemo (nome stringa, ctx context.Context) per if ok fmt.Println (name, "scadrà a:", scadenza) else fmt .Println (nome, "non ha scadenza") time.Sleep (time.Second)

La funzione principale crea tre contesti: 

  • timeoutContext con un timeout di tre secondi
  • un cancelContext senza scadenza
  • deadlineContext, che è derivato da cancelContext, con una scadenza di quattro ore da ora

Quindi, avvia la funzione contextDemo come tre goroutine. Tutti corrono simultaneamente e stampano il loro messaggio ogni secondo. 

La funzione principale attende quindi che la goroutine con il timeoutCancel venga cancellata leggendo dal suo canale Done () (bloccherà finché non sarà chiuso). Una volta scaduto il timeout dopo tre secondi, main () chiama cancelFunc () che annulla la goroutine con cancelContext e l'ultima goroutine con il contesto di scadenza quattro ore derivato.

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 contextDemo ("[cancelContext]", cancelContext) go contextDemo ( "[deadlineContext]", deadlineContext) // Attendi che scada il timeout <- 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… ")  

Ecco l'output:

[cancelContext] non ha scadenza [deadlineContext] scadrà alle: 2017-07-29 09: 06: 02.34260363 [timeoutContext] scadrà alle: 2017-07-29 05: 06: 05.342603759 [cancelContext] non ha scadenza [timeoutContext] scadenza: 2017-07-29 05: 06: 05.342603759 [deadlineContext] scadrà alle: 2017-07-29 09: 06: 02.34260363 [cancelContext] non ha scadenza [timeoutContext] scadrà tra: 2017-07-29 05: 06: 05.342603759 [deadlineContext] scadrà alle: 2017-07-29 09: 06: 02.34260363 Annullamento del contesto di cancellazione ... Il contesto di cancellazione è stato cancellato ... Il contesto di scadenza è stato cancellato ... 

Passare valori nel contesto

È possibile allegare valori a un contesto utilizzando la funzione WithValue (). Si noti che viene restituito il contesto originale, non un contesto derivato. Puoi leggere i valori dal contesto usando il metodo Value (). Modifichiamo la nostra funzione demo per ottenere il suo nome dal contesto invece di passarlo come parametro:

func contextDemo (ctx context.Context) deadline, ok: = ctx.Deadline () nome: = ctx.Value ("nome") per if ok fmt.Println (name, "scadrà a:", scadenza)  else fmt.Println (nome, "non ha scadenza") time.Sleep (time.Second) 

E modifichiamo la funzione principale per allegare il nome tramite WithValue ():

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

L'uscita rimane la stessa. Consulta la sezione sulle best practice per alcune linee guida sull'utilizzo appropriato dei valori di contesto.

Migliori pratiche

Diverse migliori pratiche sono emerse intorno ai valori del contesto:

  • Evita di passare argomenti di funzione in valori di contesto.
  • Le funzioni che desiderano memorizzare valori nel contesto in genere allocano una chiave in una variabile globale.
  • I pacchetti dovrebbero definire le chiavi come un tipo non esportato per evitare collisioni.
  • I pacchetti che definiscono una chiave di contesto devono fornire accessori di tipo sicuro per i valori memorizzati utilizzando tale chiave.

Il contesto della richiesta HTTP

Uno dei casi d'uso più utili per i contesti è il passaggio di informazioni insieme a una richiesta HTTP. Queste informazioni possono includere un ID di richiesta, credenziali di autenticazione e altro. In Go 1.7, il pacchetto standard net / http ha sfruttato il pacchetto di contesto ottenendo "standardizzato" e aggiunto il supporto contestuale direttamente all'oggetto richiesta:

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

Ora è possibile allegare un ID di richiesta dalle intestazioni fino al gestore finale in un modo standard. La funzione di gestore WithRequestID () estrae un ID di richiesta dall'intestazione "X-Request-ID" e genera un nuovo contesto con l'ID di richiesta da un contesto esistente che utilizza. Quindi lo passa al successivo gestore della catena. La funzione pubblica GetRequestID () fornisce l'accesso ai gestori che possono essere definiti in altri pacchetti.

const requestIDKey int = 0 func WithRequestID (next http.Handler) http.Handler return http.HandlerFunc (func (rw http.ResponseWriter, req * http.Request) // Estrai ID richiesta dall'intestazione della richiesta reqID: = req.Header .Get ("X-Request-ID") // Crea un nuovo contesto dal contesto della richiesta con // l'ID della richiesta ctx: = context.WithValue (req.Context (), requestIDKey, reqID) // Crea nuova richiesta con il nuovo context req = req.WithContext (ctx) // Lascia che il prossimo gestore della catena prenda il sopravvento 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 ("/", gestore) 

Conclusione

La programmazione basata sul contesto fornisce un modo standard e ben supportato per risolvere due problemi comuni: gestire la durata delle goroutine e passare informazioni fuori banda attraverso una catena di funzioni. 

Segui le migliori pratiche e usa i contesti nel giusto contesto (vedi cosa ho fatto lì?) E il tuo codice migliorerà considerevolmente.