Menu

Context en Golang : annulation, timeouts et valeurs

Comment context.Context transporte l'annulation, les échéances et les valeurs liées à une requête dans un programme Go : Background, WithCancel, WithTimeout, WithValue, ctx.Done dans un select, et le context dans les serveurs et clients HTTP.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un timeout en dix lignes

Le rôle de context.Context est de dire au code quand s'arrêter. Ici, une opération lente dispose de 50 ms et abandonne quand le context le demande :

Le premier appel se termine en 10 ms et renvoie rows <nil>. Le second aurait besoin de 200 ms, mais le context expire à 50 ms (comptées depuis sa création), donc il renvoie context deadline exceeded.

Rien n'est arrêté de force. Go n'a aucun moyen de tuer une goroutine depuis l'extérieur. Un context est un signal, et le code doit le vérifier : en faisant un select sur ctx.Done(), en vérifiant ctx.Err() entre deux étapes, ou en passant ctx à des appels de bibliothèque (http.NewRequestWithContext, db.QueryContext, exec.CommandContext) qui le vérifient pour vous.

L'interface Context

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
MéthodeRenvoie
Done()un channel fermé quand le context est annulé ou expire (nil pour un context qui ne peut jamais être annulé)
Err()nil tant qu'il est actif, puis context.Canceled ou context.DeadlineExceeded
Deadline()l'échéance et true, ou ok == false s'il n'y en a pas
Value(key)la valeur stockée sous key dans ce context ou un ancêtre, ou nil

Les contexts sont immuables. Vous n'en modifiez jamais un ; vous en dérivez un enfant avec l'une des fonctions With, et l'enfant ajoute un signal d'annulation, une échéance ou une valeur.

D'où vient un context

Tout arbre de contexts part d'une racine :

  • context.Background() pour main, init, les tests et l'initialisation de haut niveau des serveurs.
  • context.TODO() quand une fonction devrait prendre un context mais que l'appelant n'en a pas encore. Il se comporte exactement comme Background ; le nom sert de repère pour un refactoring futur.

Dans un handler HTTP, vous ne créez pas de racine. Vous utilisez r.Context(), que le serveur annule quand le client se déconnecte ou que le handler retourne.

WithCancel : arrêter à la demande

context.WithCancel renvoie un context enfant et une fonction cancel. Appeler cancel ferme le channel Done de l'enfant et les channels Done de tout ce qui en dérive.

L'envoi du producteur se trouve dans un select à côté de ctx.Done(). C'est ce qui lui permet de s'arrêter : un simple out <- i bloquerait indéfiniment dès que le consommateur cesse de lire, et la goroutine fuirait. Le for range nums final attend que le producteur ait fermé le channel. Pendant ce vidage, le producteur peut encore réussir à envoyer une ou deux valeurs, car quand les deux cas de son select sont prêts, Go en choisit un au hasard ; l'annulation est rapide, pas instantanée.

cancel peut être appelée plusieurs fois et depuis n'importe quelle goroutine. Seul le premier appel a un effet.

WithTimeout et WithDeadline

WithTimeout(parent, d) équivaut à WithDeadline(parent, time.Now().Add(d)). Utilisez un timeout pour « au maximum cette durée » et une échéance quand vous avez une heure absolue.

Une fois le délai passé, Done se ferme et Err renvoie context.DeadlineExceeded. Si cancel est appelée avant, Err renvoie context.Canceled. Vérifiez lequel avec errors.Is, car les bibliothèques enveloppent généralement l'erreur :

Appelez toujours cancel, même pour un timeout qui expirera tout seul. Le context garde un timer et une place chez son parent jusqu'à ce que l'un des deux se produise, et defer cancel() libère les deux dès que la fonction retourne. go vet signale une fonction cancel ignorée : the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.

Les enfants ne survivent pas à leurs parents

Les contexts forment un arbre. Annuler un parent annule tous ses descendants. Un enfant peut avoir une échéance plus courte que son parent, jamais plus longue : l'échéance la plus proche l'emporte toujours.

C'est ce qui rend les contexts utiles à travers les couches. Un handler HTTP reçoit un context qui meurt avec la requête ; un appel à la base de données trois couches plus bas en dérive un timeout de 2 secondes. Si le client raccroche au bout de 100 ms, la requête SQL est annulée à ce moment-là, pas deux secondes plus tard.

Faites toujours un select sur ctx.Done() quand vous bloquez

Toute goroutine qui attend (un envoi sur un channel, une réception, un timer) devrait attendre ctx.Done() en même temps. Pour les boucles qui consomment du CPU sans jamais bloquer, vérifiez ctx.Err() de temps en temps :

for i, item := range items {
	if i%1000 == 0 {
		if err := ctx.Err(); err != nil {
			return err
		}
	}
	process(item)
}

Utilisez time.After dans un select pour une attente simple, mais préférez un timer que vous pouvez arrêter (ou un timeout de context) quand l'attente risque d'être souvent annulée.

Causes d'annulation (Go 1.20 et 1.21)

ctx.Err() dit seulement canceled ou deadline exceeded. Pour enregistrer pourquoi, utilisez les variantes Cause :

WithCancelCause est arrivée avec Go 1.20, WithTimeoutCause et WithDeadlineCause avec Go 1.21. Err continue de renvoyer les valeurs standard pour que les vérifications existantes fonctionnent toujours ; context.Cause donne le détail.

WithValue, avec modération

context.WithValue(parent, key, value) attache une valeur. ctx.Value(key) la recherche en remontant la chaîne des parents.

Règles pour les valeurs :

  • Utilisez un type non exporté pour les clés, jamais une simple string. Deux packages qui utilisent tous deux "user" s'écraseraient mutuellement. (go vet ne le détecte pas ; staticcheck si.)
  • Encapsulez l'accès dans des fonctions typées comme WithRequestID et RequestID, pour que les appelants ne voient jamais any ni la clé.
  • Ne stockez que des données liées à la requête qui traversent des API : identifiants de trace et de requête, utilisateur authentifié, logger. Jamais des paramètres optionnels, des connexions à la base ou de la configuration. Ceux-là vont dans des arguments de fonction ou des champs de struct, où le compilateur peut les vérifier et les lecteurs les voir.
  • La recherche remonte la chaîne un parent à la fois, donc chaque valeur ajoutée rallonge d'un cran la recherche des autres.

Conventions

  • ctx context.Context est le premier paramètre de toute fonction qui fait des E/S, bloque, ou appelle quelque chose qui le fait : func Fetch(ctx context.Context, url string) error.
  • Ne stockez pas un context dans une struct. Passez-le à chaque appel de méthode. Un context appartient à une opération, et une struct lui survit généralement. (L'exception est un type qui représente une seule opération, comme http.Request.)
  • Ne passez jamais nil comme context. Utilisez context.TODO() si vous n'avez rien de mieux.
  • Renvoyez ctx.Err(), ou enveloppez-la avec %w, quand vous vous arrêtez à cause du context, pour que les appelants distinguent un timeout d'un vrai échec.

Le context dans les serveurs et clients HTTP

Côté serveur : r.Context() est annulé quand le client se déconnecte, quand le handler retourne, ou quand un flux HTTP/2 est réinitialisé. Côté client : http.NewRequestWithContext fait respecter un timeout ou une annulation à la requête. Ce programme fait tourner les deux côtés via httptest :

Le client abandonne à 50 ms et ferme la connexion. Le serveur s'en aperçoit, le context de sa requête est annulé, et le handler s'arrête au lieu de passer 450 millisecondes de plus sur un rapport que personne ne lira. Dans un vrai handler, vous passez r.Context() à chaque appel à la base de données et à chaque appel HTTP, et ils s'arrêtent tous ensemble.

Autres fonctions utiles (Go 1.21)

  • context.WithoutCancel(ctx) renvoie un context avec les mêmes valeurs, qui n'est pas annulé quand ctx l'est. Utilisez-le pour un travail qui doit se terminer après la fin de la requête, comme l'écriture d'un journal d'audit.
  • context.AfterFunc(ctx, f) exécute f dans sa propre goroutine une fois ctx terminé, et renvoie une fonction stop pour le désinscrire.

Erreurs courantes

  • Ne pas appeler cancel. Faites toujours defer cancel() juste après WithCancel, WithTimeout ou WithDeadline.
  • Lancer une goroutine qui ignore ctx. Si elle bloque sans select sur ctx.Done(), l'annulation n'a aucun effet et la goroutine fuit.
  • Créer un nouveau context.Background() au fond d'une chaîne d'appels. Cela coupe le lien avec l'échéance et l'annulation de l'appelant. Passez le ctx que vous avez reçu.
  • Comparer les erreurs avec ==. Utilisez errors.Is(err, context.DeadlineExceeded) ; la plupart des bibliothèques l'enveloppent.
  • Utiliser WithValue pour des dépendances. Une connexion à la base cachée dans un context est un paramètre que le compilateur ne peut plus vérifier.
  • S'attendre à une annulation instantanée. Le code ne s'en aperçoit qu'à sa prochaine vérification. Une longue boucle sans vérification continue de tourner.

Questions fréquentes

À quoi sert context en Go ?

Un context.Context indique à une fonction, et à tout ce qu'elle appelle, quand abandonner : parce que l'appelant a annulé, parce qu'une échéance est passée, ou parce que le client s'est déconnecté. Il peut aussi transporter des valeurs liées à la requête, comme un identifiant de requête. Par convention, c'est le premier paramètre, nommé ctx.

Quelle est la différence entre context.Background et context.TODO ?

Les deux renvoient un context vide qui n'est jamais annulé et n'a ni échéance ni valeurs. Ils se comportent de façon identique. Background() est la racine pour main, les tests et l'initialisation de haut niveau. TODO() marque un endroit où un vrai context devrait être passé mais où le code environnant n'en a pas encore, ce qui le rend facile à retrouver plus tard.

Pourquoi faut-il appeler cancel après context.WithTimeout ?

WithTimeout, WithDeadline et WithCancel enregistrent le nouveau context auprès de son parent et peuvent démarrer un timer. Appeler cancel libère ces ressources dès que vous avez terminé, au lieu d'attendre que le timeout expire ou que le parent soit annulé. Écrivez defer cancel() juste après la création ; go vet avertit quand une fonction cancel est ignorée.

Que signifie « context deadline exceeded » en Go ?

C'est le texte de context.DeadlineExceeded, l'erreur que renvoie ctx.Err() une fois l'échéance d'un context passée. Les fonctions qui respectent le context, comme les clients HTTP et les drivers de base de données, la renvoient (souvent enveloppée) quand elles manquent de temps. Testez-la avec errors.Is(err, context.DeadlineExceeded).

Faut-il utiliser context.WithValue pour passer des paramètres ?

Non. Réservez-le aux données liées à la requête qui traversent les frontières d'API et que les fonctions intermédiaires n'ont pas à connaître, comme un identifiant de trace ou un utilisateur authentifié. Tout ce dont une fonction a besoin pour faire son travail va dans ses paramètres, là où le compilateur le vérifie.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER