Comprendre le package de contexte dans Golang

image


Le package de contexte dans Go est utile pour les interactions avec les API et les processus lents, en particulier dans les systèmes de production qui traitent les demandes Web. Avec son aide, les goroutines peuvent être informées de la nécessité de terminer leur travail.


Vous trouverez ci-dessous un petit guide pour vous aider à utiliser ce package dans vos projets, ainsi que certaines des meilleures pratiques et écueils.


(Remarque: le contexte est utilisé dans de nombreux packages, par exemple pour travailler avec Docker ).


Avant de commencer


Pour utiliser des contextes, vous devez comprendre ce que sont le goroutine et les canaux. Je vais essayer de les considérer brièvement. Si vous les connaissez déjà, allez directement à la section Contexte.


Gorutin


La documentation officielle indique que «Gorutin est un flux d'exécution léger». Les goroutines sont plus légères que les fils, leur gestion nécessite donc relativement moins de ressources.


Bac à sable


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") } 

Si vous exécutez ce programme, vous verrez que seul Hello from main imprimé. En fait, les deux goroutines commencent, mais le main termine plus tôt. Ainsi, les Goroutines ont besoin d'un moyen d'informer le main de la fin de leur exécution, et pour qu'elle attende cela. Ici, les canaux viennent à notre secours.


Chaînes


Les canaux sont un moyen de communication entre les goroutins. Ils sont utilisés lorsque vous souhaitez transférer des résultats, des erreurs ou d'autres informations d'un goroutine à un autre. Les canaux sont de types différents, par exemple, un canal de type int reçoit des entiers, et un canal de type error reçoit des erreurs, etc.


Disons que nous avons un canal ch de type int . Si vous voulez envoyer quelque chose au canal, la syntaxe sera ch <- 1 . Vous pouvez obtenir quelque chose du canal comme ceci: var := <- ch , i.e. prendre la valeur du canal et l'enregistrer dans la variable var .


Le code suivant illustre comment utiliser les canaux pour confirmer que les goroutines ont terminé leur travail et retourné leurs valeurs à main .


Remarque: Les groupes d'attente peuvent également être utilisés pour la synchronisation, mais dans cet article, j'ai sélectionné des canaux pour des exemples de code, car nous les utiliserons plus tard dans la section contextuelle.


Bac à sable


 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 } 

Contexte


Le package de contexte dans go vous permet de transmettre des données à votre programme dans une sorte de «contexte». Le contexte, comme un délai d'attente, une échéance ou un canal, signale un arrêt et le retour des appels.


Par exemple, si vous effectuez une demande Web ou exécutez une commande système, ce serait une bonne idée d'utiliser un délai d'expiration pour les systèmes de production. Parce que si l'API à laquelle vous accédez est lente, il est peu probable que vous souhaitiez accumuler des demandes dans votre système, car cela peut entraîner une charge accrue et une baisse des performances lors du traitement de vos propres demandes. Le résultat est un effet en cascade.


Et ici, le contexte de délai d'attente ou de délai peut être juste.


Création de contexte


Le package de contexte vous permet de créer et d'hériter du contexte des manières suivantes:


context.Background () Contexte ctx


Cette fonction renvoie un contexte vide. Il ne doit être utilisé qu'à un niveau élevé (dans le gestionnaire de requêtes principal ou le plus élevé). Il peut être utilisé pour obtenir d'autres contextes, dont nous discuterons plus tard.


 ctx, cancel := context.Background() 

Remarque trans.: Il y a une inexactitude dans l'article original, l'exemple correct d'utilisation du context.Background sera le suivant:


 ctx := context.Background() 

context.TODO () Contexte ctx


Cette fonction crée également un contexte vide. Et il doit également être utilisé uniquement à un niveau élevé, soit lorsque vous n'êtes pas sûr du contexte à utiliser, soit si la fonction ne reçoit pas encore le contexte souhaité. Cela signifie que vous (ou quelqu'un qui prend en charge le code) prévoyez d'ajouter du contexte à la fonction ultérieurement.


 ctx, cancel := context.TODO() 

Remarque trans.: Il y a une inexactitude dans l'article d'origine, l'exemple correct d'utilisation du context.TODO sera le suivant:


 ctx := context.TODO() 

Fait intéressant, jetez un œil au code , il est absolument identique à l'arrière-plan. La seule différence est que dans ce cas, vous pouvez utiliser les outils d'analyse statique pour vérifier la validité du transfert de contexte, ce qui est un détail important, car ces outils aident à identifier les erreurs potentielles à un stade précoce et peuvent être inclus dans le pipeline CI / CD.


D'ici :


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

context.WithValue (Contexte parent, clé, interface val {}) (Contexte ctx, annuler CancelFunc)


Remarque Lane: Il y a une inexactitude dans l'article d'origine, la signature correcte pour le context.WithValue sera la suivante:


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

Cette fonction prend un contexte et retourne un contexte dérivé de celui-ci dans lequel la valeur val est associée à la key et traverse l'arborescence de contexte entière. Autrement dit, dès que vous créez un contexte WithValue , tout contexte dérivé recevra cette valeur.


Il n'est pas recommandé de passer des paramètres critiques à l'aide de valeurs de contexte; à la place, les fonctions doivent les prendre explicitement dans la signature.


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

context.WithCancel (Contexte parent) (Contexte ctx, annuler CancelFunc)


Cela devient un peu plus intéressant ici. Cette fonction crée un nouveau contexte à partir du parent qui lui a été transmis. Le parent peut être le contexte d'arrière-plan ou le contexte passé en argument à la fonction.


Le contexte dérivé et la fonction d'annulation sont retournés. Seule la fonction qui la crée doit appeler la fonction pour annuler le contexte. Vous pouvez passer la fonction d'annulation à d'autres fonctions si vous le souhaitez, mais cela est fortement déconseillé. Habituellement, cette décision est prise à partir d'une mauvaise compréhension de l'annulation du contexte. Pour cette raison, les contextes générés à partir de ce parent peuvent affecter le programme, ce qui entraînera un résultat inattendu. Bref, il vaut mieux ne JAMAIS passer une fonction d'annulation.


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

Remarque Lane: Dans l'article d'origine, l'auteur, apparemment, par erreur pour context.WithCancel donné un exemple avec context.WithDeadline . L'exemple correct pour context.WithCancel serait:


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

context.WithDeadline (Contexte parent, d time.Time) (Contexte ctx, annuler CancelFunc)


Cette fonction renvoie un contexte dérivé de son parent, qui est annulé après une échéance ou un appel à la fonction d'annulation. Par exemple, vous pouvez créer un contexte qui est automatiquement annulé à un moment spécifique et le transmettre aux fonctions enfants. Lorsque ce contexte est annulé après la date limite, toutes les fonctions qui ont ce contexte doivent être notifiées par notification.


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

context.WithTimeout (Contexte parent, timeout time.Duration) (Contexte ctx, annuler CancelFunc)


Cette fonction est similaire à context.WithDeadline. La différence est que la durée est utilisée comme entrée. Cette fonction renvoie un contexte dérivé qui est annulé lorsque la fonction d'annulation est appelée ou après un certain temps.


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

Remarque Lane: Dans l'article d'origine, l'auteur, apparemment, par erreur pour context.WithTimeout donné un exemple avec context.WithDeadline . L'exemple correct pour context.WithTimeout serait le suivant:


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

Réception et utilisation des contextes dans vos fonctions


Maintenant que nous savons comment créer des contextes (Background et TODO) et comment générer des contextes (WithValue, WithCancel, Deadline et Timeout), voyons comment les utiliser.


Dans l'exemple suivant, vous pouvez voir que la fonction qui prend le contexte lance le goroutine et s'attend à ce qu'il retourne ou annule le contexte. L'instruction select nous aide à déterminer ce qui se passe en premier et à terminer la fonction.


Après avoir fermé le canal Terminé <-ctx.Done() , le cas de case <-ctx.Done(): est sélectionné. Dès que cela se produit, la fonction doit interrompre le travail et préparer un retour. Cela signifie que vous devez fermer toutes les connexions ouvertes, libérer des ressources et revenir de la fonction. Il y a des moments où la libération des ressources peut retarder le retour, par exemple, le nettoyage se bloque. Vous devez garder cela à l'esprit.


L'exemple qui suit cette section est un programme go entièrement terminé qui illustre les délais d'expiration et les fonctions d'annulation.


 // ,  -      // ,   -    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") } } 

Exemple


Comme nous l'avons vu, en utilisant des contextes, vous pouvez travailler avec des délais, des délais d'expiration et également appeler la fonction d'annulation, ce qui indique clairement à toutes les fonctions utilisant un contexte dérivé que vous devez terminer votre travail et exécuter le retour. Prenons un exemple:


fonction main :


  • Crée un contexte de fonction d'annulation
  • Appelle la fonction d'annulation après un délai arbitraire

Fonction doWorkContext :


  • Crée un contexte dérivé avec un délai d'expiration
  • Ce contexte est annulé lorsque la fonction principale appelle cancelFunction, le délai expire ou doWorkContext appelle son cancelFunction.
  • Exécute goroutine pour effectuer une tâche lente, en passant le contexte résultant
  • Attend que les goroutines se terminent ou que le contexte soit annulé du principal, selon la première éventualité

Fonction sleepRandomContext :


  • Lance goroutine pour effectuer une tâche lente
  • Attend la fin du goroutine, ou
  • Attend que le contexte soit annulé par la fonction principale, expire ou appelle sa propre fonction d'annulation

Fonction sleepRandom :


  • S'endort à un moment aléatoire

Cet exemple utilise le mode veille pour simuler un temps de traitement aléatoire, mais en réalité, vous pouvez utiliser des canaux pour signaler cette fonction sur le début du nettoyage et attendre la confirmation du canal que le nettoyage est terminé.


Sandbox (Il semble que le temps aléatoire que j'utilise dans le sandbox soit pratiquement inchangé. Essayez ceci sur votre ordinateur local pour voir le caractère aléatoire)


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) } 

Pièges


Si la fonction utilise le contexte, assurez-vous que les notifications d'annulation sont gérées correctement. Par exemple, cet exec.CommandContext ne ferme pas le canal de lecture tant que la commande n'a pas terminé toutes les fourches créées par le processus ( Github ), c'est-à-dire que l'annulation du contexte ne revient pas immédiatement de la fonction si vous attendez avec cmd.Wait (), jusqu'à ce que toutes les fourches de la commande externe terminent le traitement.


Si vous utilisez un délai d'expiration ou une échéance avec une durée d'exécution maximale, cela peut ne pas fonctionner comme prévu. Dans de tels cas, il est préférable d'implémenter des délais d'attente en utilisant le time.After .


Meilleures pratiques


  1. context.Background ne doit être utilisé qu'au plus haut niveau, en tant que racine de tous les contextes dérivés.
  2. context.TODO doit être utilisé lorsque vous ne savez pas quoi utiliser, ou si la fonction actuelle utilisera le contexte à l'avenir.
  3. Les annulations de contexte sont recommandées, mais ces fonctions peuvent prendre un certain temps pour effacer et quitter.
  4. context.Value doit être utilisé avec parcimonie et ne doit pas être utilisé pour transmettre des paramètres facultatifs. Cela rend l'API incompréhensible et peut entraîner des erreurs. Ces valeurs doivent être transmises comme arguments.
  5. Ne stockez pas les contextes dans une structure; passez-les explicitement dans les fonctions, de préférence comme premier argument.
  6. Ne passez jamais un contexte nul comme argument. En cas de doute, utilisez TODO.
  7. La structure de Context n'a pas de méthode d'annulation, car seule la fonction qui génère le contexte doit l'annuler.

Du traducteur


Dans notre entreprise, nous utilisons activement le package Context lors du développement d'applications serveur à usage interne. Mais de telles applications pour un fonctionnement normal, en plus de Context, nécessitent des éléments supplémentaires, tels que:


  • Journalisation
  • Traitement du signal pour la terminaison, le rechargement et la rotation de l'application
  • Travailler avec des fichiers pid
  • Travailler avec des fichiers de configuration
  • Et autres

Par conséquent, à un moment donné, nous avons décidé de résumer toute l'expérience que nous avions accumulée et de créer des packages auxiliaires qui ont grandement simplifié la rédaction des applications (en particulier les applications qui ont des API). Nous avons publié nos développements dans le domaine public et tout le monde peut les utiliser. Voici quelques liens vers des packages utiles pour résoudre ces problèmes:



Lisez également d'autres articles sur notre blog:


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


All Articles