
Un appel réseau sans garde-fou peut bloquer une goroutine pour toujours ; context.Context transporte un signal d'annulation et un délai à toute la chaîne d'appels. Ce guide vous apprend à créer un contexte annulable avec WithCancel, WithTimeout et WithDeadline, à réagir à l'annulation via ctx.Done() et ctx.Err(), à couper un appel HTTP trop lent, à propager le contexte de fonction en fonction, et à n'utiliser context.Value que pour ce à quoi il sert vraiment. Public visé : vous maîtrisez déjà les goroutines et la synchronisation. Exemples testés sur Go 1.26.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Créer un contexte avec
WithCancel,WithTimeoutouWithDeadline. - Réagir à l'annulation avec
ctx.Done()etctx.Err(). - Couper un appel réseau trop lent et propager le contexte dans la chaîne d'appels.
- Éviter le mauvais usage de
context.Value.
Pourquoi un contexte
Section intitulée « Pourquoi un contexte »Imaginez une fonction qui interroge un service distant lent. Si l'utilisateur ferme sa connexion ou si un délai raisonnable est dépassé, rien n'arrête l'appel en cours : la goroutine reste bloquée, à consommer de la mémoire, parfois pour toujours. Un context.Context résout ce problème en transportant un signal d'annulation partagé par toute la chaîne d'appels. Quand la racine annule, chaque fonction qui écoute le contexte l'apprend et peut abandonner proprement.
Le contexte sert trois besoins : annuler manuellement une opération, lui imposer un délai, et véhiculer quelques données propres à la requête (un identifiant de trace). C'est la colonne vertébrale de tout serveur ou client réseau écrit en Go.
Créer un contexte annulable
Section intitulée « Créer un contexte annulable »Un contexte se dérive toujours d'un parent, le plus souvent la racine context.Background(). Trois fabriques renvoient chacune un contexte enfant et une fonction cancel à appeler pour libérer les ressources associées :
// Annulation manuelle, déclenchée par un appel à cancel().ctx, cancel := context.WithCancel(context.Background())defer cancel()
// Annulation automatique après une durée.ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()
// Annulation à une date absolue.ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))defer cancel()Retenez la règle qui évite le bug le plus courant : appelez toujours cancel, idéalement en defer juste après la création. Même pour un WithTimeout qui expirera de lui-même, cancel libère la goroutine interne de surveillance. L'oublier provoque une fuite de contexte.
Réagir à l'annulation avec Done et Err
Section intitulée « Réagir à l'annulation avec Done et Err »Une fonction qui respecte un contexte écoute son channel ctx.Done(), fermé au moment de l'annulation, généralement dans un select. La méthode ctx.Err() indique ensuite pourquoi : elle renvoie context.Canceled si cancel a été appelé, context.DeadlineExceeded si le délai a expiré, et nil tant que le contexte est actif.
func worker(ctx context.Context, id int, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-ctx.Done(): // annulation reçue fmt.Printf("worker %d arrêté: %v\n", id, ctx.Err()) return case <-time.After(50 * time.Millisecond): // petit travail répété } }}En lançant trois workers puis en appelant cancel() une seule fois, les trois s'arrêtent ensemble : le même contexte a diffusé le signal à tous.
worker 3 arrêté: context canceledworker 1 arrêté: context canceledworker 2 arrêté: context canceledtous les workers sont arrêtésCouper un appel réseau avec WithTimeout
Section intitulée « Couper un appel réseau avec WithTimeout »C'est l'usage numéro un du contexte. http.NewRequestWithContext attache le contexte à la requête : si le délai expire avant la réponse, le client interrompt l'appel au lieu d'attendre indéfiniment. Voici un client qui vise un serveur répondant en 300 ms, d'abord avec un délai trop court, puis avec un délai suffisant :
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)defer cancel()req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)_, err := http.DefaultClient.Do(req)fmt.Println("délai court:", err)Avec 100 ms face à un serveur qui met 300 ms, l'appel est annulé et l'erreur le dit clairement :
délai court: Get "http://127.0.0.1:...": context deadline exceededPorté à 1 seconde, le même appel réussit et renvoie le corps de la réponse. Le contexte ne rend pas le serveur plus rapide : il vous rend maître du temps que vous acceptez d'attendre, ce qui est la base d'un client réseau robuste.
Le contexte d'une requête serveur
Section intitulée « Le contexte d'une requête serveur »Côté serveur, chaque requête HTTP arrive déjà avec son propre contexte, accessible par r.Context(). Ce contexte est annulé automatiquement quand le client se déconnecte ou renonce. Un handler qui exécute un travail long a donc intérêt à l'écouter, pour ne pas continuer à calculer une réponse que plus personne n'attend.
func handler(w http.ResponseWriter, r *http.Request) { select { case <-time.After(1 * time.Second): fmt.Fprintln(w, "travail terminé") case <-r.Context().Done(): fmt.Println("serveur: client parti ->", r.Context().Err()) return }}Si le client abandonne au bout de 100 ms, le serveur le détecte aussitôt :
client: Get "http://127.0.0.1:...": context deadline exceededserveur: client parti -> context canceledLe même contexte relie les deux bouts de la chaîne : l'annulation décidée par le client devient, côté serveur, un context canceled qui interrompt le travail devenu inutile. C'est aussi ce r.Context() que vous transmettez à vos requêtes en base de données ou à vos clients d'API en aval, pour que l'annulation se propage jusqu'au bout.
Propager le contexte dans la chaîne d'appels
Section intitulée « Propager le contexte dans la chaîne d'appels »La convention Go est stricte et universelle : le contexte est toujours le premier paramètre d'une fonction, nommé ctx. On le passe de couche en couche jusqu'à l'opération bloquante, si bien qu'une annulation décidée en haut se propage à tout l'arbre d'appels.
func couche1(ctx context.Context) error { return couche2(ctx) // on transmet le même contexte}
func couche2(ctx context.Context) error { select { case <-time.After(200 * time.Millisecond): return nil case <-ctx.Done(): return fmt.Errorf("couche2 annulée: %w", ctx.Err()) }}Avec un contexte muni d'un délai de 100 ms, couche2 n'atteint jamais ses 200 ms de travail : l'annulation remonte, enveloppée avec %w pour rester comparable via errors.Is.
resultat couche1: couche2 annulée: context deadline exceededest DeadlineExceeded: truecontext.Value, à réserver aux données de requête
Section intitulée « context.Value, à réserver aux données de requête »context.WithValue attache une donnée au contexte, disponible dans toute la chaîne via ctx.Value(clé). C'est tentant pour tout faire passer, mais l'outil est volontairement limité : aucune sécurité de type, et une recherche linéaire à chaque lecture. Réservez-le aux données propres à la requête (identifiant de trace, informations d'authentification), jamais aux paramètres de vos fonctions, qui doivent rester des arguments explicites.
Deux règles rendent son usage sûr. D'abord, la clé doit être d'un type non exporté, pour éviter toute collision entre paquets. Ensuite, encapsulez lecture et écriture dans des fonctions typées plutôt que de manipuler Value à nu, car une assertion de type ratée provoque une panique.
// Clé de contexte typée et privée au paquet.type cleContexte string
const cleTrace cleContexte = "traceID"
ctx := context.WithValue(context.Background(), cleTrace, "abc-123")fmt.Println("traceID lu:", ctx.Value(cleTrace)) // abc-123Erreurs fréquentes
Section intitulée « Erreurs fréquentes »Le contexte est simple en apparence, mais quelques pièges reviennent sans cesse dans les questions des développeurs Go. Là encore, go vet en attrape une partie avant l'exécution.
| Symptôme | Cause | Correction |
|---|---|---|
| Fuite de goroutine, ressources jamais libérées | cancel d'un WithCancel / WithTimeout non appelé | defer cancel() juste après la création ; go vet signale le cancel perdu |
| Contexte « mort » dès la 2ᵉ requête | contexte stocké dans une struct de longue durée | Passer ctx en argument, jamais le mémoriser dans un champ |
panic sur une assertion de type | valeur lue avec context.Value d'un type inattendu | Encapsuler dans des getters typés ; ne pas s'en servir de fourre-tout |
| Collision de valeurs entre paquets | clé de WithValue d'un type primitif (string, int) | Utiliser un type non exporté comme clé |
| Appel qui bloque malgré le contexte | l'opération n'écoute pas ctx.Done() | Intégrer <-ctx.Done() dans le select, ou utiliser une API compatible contexte |
Le piège le plus fréquent reste l'oubli de cancel. Comme WithTimeout et WithDeadline lancent une goroutine interne pour surveiller l'échéance, ne pas appeler cancel la laisse vivre jusqu'à l'expiration, voire au-delà. C'est une fuite de contexte que go vet détecte : the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak. Le réflexe defer cancel() immédiat coûte une ligne et supprime la classe entière de bugs.
Le second piège est plus insidieux : stocker un contexte dans une structure qui vit plus longtemps que la requête. Le contexte du premier appel y reste figé, il est annulé à la fin de cette requête, et toutes les suivantes héritent d'un contexte déjà mort. La documentation Go est catégorique : le contexte se transmet en argument, il ne se conserve pas dans un objet.
Exercice : une opération annulable
Section intitulée « Exercice : une opération annulable »Assemblez timeout et propagation. Cherchez la solution avant d'ouvrir la réponse.
Écrivez une fonction traiter(ctx context.Context, n int) error qui simule un travail de n millisecondes avec un select sur time.After et ctx.Done() :
- si le travail se termine avant l'annulation, renvoyer
nil. - sinon, renvoyer l'erreur du contexte, enveloppée avec
%w.
Dans main, appelez-la avec un contexte à 50 ms de délai et un travail de 200 ms. Le résultat attendu contient context deadline exceeded, et errors.Is(err, context.DeadlineExceeded) doit valoir true.
Indice : n'oubliez pas defer cancel().
Le select départage la fin du travail et l'annulation.
package main
import ( "context" "errors" "fmt" "time")
func traiter(ctx context.Context, n int) error { select { case <-time.After(time.Duration(n) * time.Millisecond): return nil case <-ctx.Done(): return fmt.Errorf("traitement interrompu: %w", ctx.Err()) }}
func main() { ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel()
err := traiter(ctx, 200) fmt.Println("resultat:", err) fmt.Println("deadline dépassée:", errors.Is(err, context.DeadlineExceeded))}resultat: traitement interrompu: context deadline exceededdeadline dépassée: trueLe travail de 200 ms n'aboutit jamais : à 50 ms, le contexte ferme Done(), le select bascule et remonte l'erreur. L'enveloppe %w préserve la sentinelle context.DeadlineExceeded pour errors.Is.
À retenir
Section intitulée « À retenir »- Un
context.Contexttransporte un signal d'annulation et un délai à toute la chaîne d'appels. WithCancel,WithTimeoutetWithDeadlinedérivent un contexte enfant ; appelez toujourscancel, endefer.- On écoute
ctx.Done()dans unselect;ctx.Err()renvoiecontext.Canceledoucontext.DeadlineExceeded. - Le contexte est toujours le premier argument (
ctx) et se transmet, jamais stocké dans une struct ni passé ànil. context.Valueest réservé aux données de requête, avec une clé de type non exporté et des accès typés.go vetrepère lecanceloublié (fuite de contexte).