Aller au contenu
Développement medium

Le contexte (context.Context) en Go

14 min de lecture

Go

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.

  • Créer un contexte avec WithCancel, WithTimeout ou WithDeadline.
  • Réagir à l'annulation avec ctx.Done() et ctx.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.

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.

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.

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 canceled
worker 1 arrêté: context canceled
worker 2 arrêté: context canceled
tous les workers sont arrêtés

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 exceeded

Porté à 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.

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 exceeded
serveur: client parti -> context canceled

Le 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.

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 exceeded
est DeadlineExceeded: true

context.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-123

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ômeCauseCorrection
Fuite de goroutine, ressources jamais libéréescancel 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êtecontexte stocké dans une struct de longue duréePasser ctx en argument, jamais le mémoriser dans un champ
panic sur une assertion de typevaleur lue avec context.Value d'un type inattenduEncapsuler dans des getters typés ; ne pas s'en servir de fourre-tout
Collision de valeurs entre paquetsclé de WithValue d'un type primitif (string, int)Utiliser un type non exporté comme clé
Appel qui bloque malgré le contextel'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.

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().

  • Un context.Context transporte un signal d'annulation et un délai à toute la chaîne d'appels.
  • WithCancel, WithTimeout et WithDeadline dérivent un contexte enfant ; appelez toujours cancel, en defer.
  • On écoute ctx.Done() dans un select ; ctx.Err() renvoie context.Canceled ou context.DeadlineExceeded.
  • Le contexte est toujours le premier argument (ctx) et se transmet, jamais stocké dans une struct ni passé à nil.
  • context.Value est réservé aux données de requête, avec une clé de type non exporté et des accès typés.
  • go vet repère le cancel oublié (fuite de contexte).

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn