Accueil / Articles / Contexts en Go : le guide complet et pratique pour des services robustes
Contexts en Go : le guide complet et pratique pour des services robustes
Publié le 30 Septembre 2025
Si tu écris du code Go en production, tu tombes rapidement sur ce petit invité nommé context. On le voit partout : en paramètre des handlers HTTP, dans database/sql, en gRPC, dans les clients cloud, et même au cœur de tes propres fonctions. Mais à quoi sert-il vraiment ? Pourquoi est-il devenu un standard de facto dans l’écosystème Go ? Et comment l’utiliser proprement sans transformer ton code en passoire à valeurs globales ?
Dans cet article, nous allons décortiquer les contexts (oui, le nom du paquet est littéralement context) de manière pragmatique : le pourquoi, le comment, les pièges à éviter, les alternatives à connaître, et les perspectives d’évolution. L’objectif : t’aider à écrire des services plus robustes, observables et faciles à maintenir.
Pourquoi context répond à un vrai besoin en Go (et dans le web)
Le problème : coordonner l’arrêt et le temps dans un code concurrent
Go brille pour sa concurrence légère : on lance des goroutines à la pelle, on parle via des channels, on compose des services… jusqu’au jour où on se demande comment tout arrêter proprement. Sans mécanisme partagé, chaque goroutine vit sa meilleure vie et continue d’exécuter du travail même si la requête HTTP côté client a été annulée, si un timeout doit s’appliquer, ou si le serveur doit se fermer.
Résultat : fuites de goroutines (elles continuent de tourner sans supervision), sockets/connexions laissées ouvertes (HTTP, gRPC, DB) et jobs orphelins qui poursuivent leur travail après l’annulation de la requête. Au bout de la chaîne : pools de connexions épuisés, latence qui grimpe, erreurs en cascade et CPU/mémoire consommés pour rien.
Dans les applications web avec API REST, gRPC, GraphQL et services backend, le besoin est universel : il s’agit de pouvoir annuler rapidement les opérations lorsque l’utilisateur n’en a plus besoin. Par exemple lorsque l’onglet est fermé, d’imposer un délai maximal (timeout) afin qu’une dépendance lente n’immobilise pas toute la chaîne, et de propager ce même signal d’annulation et ces contraintes de durée aux sous‑appels ainsi qu’aux goroutines filles pour que l’ensemble du travail lié à la requête s’interrompe de concert. Il s’agit aussi de transporter de petites métadonnées comme un trace ID ou un user ID, sans recourir à des variables globales.
Sans un langage commun entre bibliothèques, chacun réinvente sa roue (flags d’arrêt, channels spécifiques, booléens atomiques…). Cela complique l’interopérabilité et favorise les bugs.
La réponse de Go : un contrat minimal et universel
Le paquet context propose une interface minuscule : Done() <-chan struct{} expose un canal qui se ferme lorsqu’il faut arrêter, ce qui sert de signal d’arrêt coopératif à toutes les goroutines concernées. Err() error fournit la raison de l’arrêt, et permet de distinguer une annulation volontaire (context.Canceled()) d’un dépassement d’échéance (context.DeadlineExceeded()). Deadline() (time.Time, bool) indique, lorsqu’elle existe, l’échéance à partir de laquelle le contexte doit expirer ; cette date permet d’aligner les délais tout au long de la pile d’appels. Enfin, Value(key any) any transporte de petites valeurs liées à la requête comme un identifiant de trace ou un identifiant utilisateur et doit rester un mécanisme parcimonieux pour éviter d’y glisser des paramètres métier ou des objets volumineux.
Ce contrat est composable et propagable : un contexte parent peut créer un contexte enfant avec annulation (WithCancel), délai (WithTimeout), ou échéance (WithDeadline). Toute fonction qui reçoit ctx sait immédiatement comment se comporter en cas d’arrêt ou de dépassement de délai sans dépendre de ton implémentation maison.
Bénéfices concrets côté backend
Sur le plan de la robustesse, Context empêche l’apparition de goroutines zombies et réduit les fuites de ressources : quand une requête est interrompue, le signal remonte jusqu’aux couches profondes et met fin aux travaux associés de manière ordonnée. Du point de vue des performances, l’annulation et les deadlines libèrent plus vite les connexions et la mémoire lorsque le résultat n’est plus utile, ce qui améliore la réactivité globale du service sous charge. En matière d’observabilité, l’usage systématique de Context uniformise la façon dont les timeouts et annulations se manifestent, ce qui rend leur traçage et leur corrélation beaucoup plus fiables dans les logs et les traces distribuées.
Enfin, côté interopérabilité, l’écosystème Go ayant adopté ce contrat, les bibliothèques et dépendances parlent le même langage : les API HTTP, database/sql, gRPC ou les SDK cloud s’imbriquent naturellement autour du même ctx.
func fetchUser(ctx context.Context, db *sql.DB, id int64) (User, error) {
// 200 ms max pour cette requête DB
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
var u User
err := db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id=$1`, id).Scan(&u.ID, &u.Email)
if err != nil {
return User{}, err
}
return u, nil
}
Ici, si la requête HTTP est annulée ou si la base de données rame, la requête SQL est automatiquement interrompue.
Historique et philosophie
D’où vient context ?
Le paquet context est né d’un constat chez Google (d’où vient Go) : à grande échelle, des microservices en cascade, des RPC imbriquées, des jobs distribués… tout dépend d’opérations annulables avec des délais cohérents. Les premières versions de Go popularisent Context via golang.org/x/net/context. Context entre dans la librairie standard et y reste depuis (y compris dans les versions récentes comme Go 1.25), ce qui scelle son rôle central.
Les objectifs de la librairie
Le paquet context vise d’abord une simplicité extrême. Son interface est volontairement minuscule afin que toute bibliothèque puisse l’adopter sans friction et que chaque développeur comprenne instantanément les attentes contractuelles d’une fonction. Cette sobriété rend les APIs plus lisibles : en voyant ctx en premier paramètre, on sait d’emblée que l’opération peut être annulée, temporisée et qu’elle peut véhiculer de petites métadonnées. Cette simplicité n’est pas une coquetterie ; c’est un choix de design qui favorise l’uniformité dans l’écosystème et abaisse le coût cognitif au quotidien.
Le deuxième principe est la propagation descendante. Un contexte parent crée des enfants via WithCancel(), WithTimeout() ou WithDeadline(), et cette relation hiérarchique organise naturellement le cycle de vie d’une requête. Lorsque l’appelant annule, toutes les sous tâches sont notifiées et se terminent proprement ; lorsqu’une échéance arrive, l’ensemble de l’arborescence s’éteint sans fuite. Cette propagation structure le travail concurrent comme un arbre : chaque nœud hérite du destin du parent, ce qui simplifie l’écriture d’arrêts propres.
Troisième pilier, l’annulation coopérative. Go ne tente pas de « tuer » une goroutine de l’extérieur ; ce serait dangereux et imprévisible. À la place, Context émet un signal via Done() que les fonctions observent ponctuellement à des points sûrs, typiquement dans des boucles ou lors d’attentes prolongées. Ainsi, les fonctions libèrent leurs ressources, restituent les connexions et retournent rapidement ctx.Err() quand on leur demande d’arrêter. Le résultat est un système plus prévisible, plus stable et plus respectueux des invariants de tes dépendances (base de données, clients réseau, etc…).
Quatrième intention, le découplage. Context définit un contrat minimal entre appelants et appelés : on n’impose ni framework ni modèle d’exécution spécifique. Les bibliothèques n’ont pas besoin de connaître les choix en termes d’architecture de code pour rester annulables et temporisées ; elles se contentent d’accepter ctx et d’en respecter les signaux. Ce découplage améliore la testabilité, facilite l’inversion de dépendances et permet d’assembler des composants hétérogènes sans convenir d’un protocole maison.
Enfin, Context propose un mécanisme de clés/valeurs (à manier avec parcimonie). L’objectif n’est pas de transporter des paramètres métier, mais de véhiculer de petites données immuables qui décrivent l’exécution : un identifiant de trace, une locale, quelques claims d’authentification. Ces informations doivent rester légères et être accessibles partout où le contexte passe, sans pour autant masquer les vrais arguments d’une fonction. Utiliser des clés typées privées et préserver l’immuabilité garantit la sécurité de ce canal et maintient la lisibilité des APIs.
Pris ensemble, ces objectifs expliquent pourquoi context est devenu la colonne vertébrale des services web en Go : une surface minime, une propagation hiérarchique claire, un arrêt coopératif, un découplage strict et un canal minimal de métadonnées. Cet ensemble tient parce qu’il est modeste. En refusant de tout faire, context permet au reste de l’écosystème d’être simple, prévisible et interopérable.
Fonctionnalités de context en Go
Les constructeurs
context.Background() est la racine vide idéale pour initialiser un contexte lorsqu’on démarre une application dans main, qu’on écrit un test ou un script d’amorçage. C’est un point de départ stable, long‑vécu, qui n’encode ni délai ni annulation et qui convient à la création des serveurs, des clients ou des workers.
context.TODO() est une fonction de la bibliothèque standard (présente depuis Go 1.7) qui sert de marqueur temporaire lorsque l’on sait qu’un vrai contexte appelant manque encore. Il ne faut pas le confondre avec context.Background() : le premier signale « à remplacer » et doit disparaître avant la mise en production au profit du contexte réel (par exemple r.Context() côté HTTP), tandis que le second fournit une racine stable et assumée.
context.WithCancel(parent) crée un enfant annulable manuellement via la fonction de retour cancel(). Cet enfant hérite des propriétés du parent et peut être coupé immédiatement quand l’appelant décide d’arrêter un sous‑travail. On appelle systématiquement defer cancel() pour libérer les ressources même si l’annulation ne survient pas.
context.WithTimeout(parent, d) produit un enfant avec une échéance relative. À l’expiration de d, le contexte est automatiquement annulé et signale context.DeadlineExceeded. Malgré cette annulation automatique, on appelle tout de même cancel() en defer afin de libérer les timers internes et éviter les fuites.
context.WithDeadline(parent, t) est l’équivalent à échéance absolue. Cette variante est utile lorsque plusieurs composants doivent s’aligner sur une même horloge ou lorsqu’un délai global imposé par l’appelant doit être respecté de bout en bout.
context.WithValue(parent, key, val) associe une valeur au contexte courant. Son usage doit rester frugal et se limiter à des métadonnées légères, immuables et liées à la requête, comme un identifiant de trace ou une locale. On évite d’y placer des paramètres métier ou des objets volumineux, et l’on préfère des clés typées privées pour prévenir les collisions.
Les signaux d’annulation
Done() se ferme (ne reçoit jamais) lorsque l’opération doit cesser. En pratique, on structure l’attente de manière coopérative avec un select qui surveille ctx.Done() en parallèle du travail utile.
Err() retourne ensuite la raison de l’arrêt, typiquement context.Canceled pour une annulation explicite ou context.DeadlineExceeded quand l’échéance est dépassée.
Pourquoi pas des variables globales ou des singletons ?
Les variables globales ou les singletons donnent l’illusion d’une simplicité immédiate, mais elles déplacent la complexité hors du regard du lecteur. Ce sont des dépendances invisibles : un package peut écrire dans un logger global, accéder à une connexion de base de données globale ou lire une configuration sans que la signature de la fonction ne le signale. Il devient difficile de visualiser les effets de bord. À l’inverse, Context rend ces contraintes visibles : une fonction qui accepte ctx annonce explicitement qu’elle peut être annulée, temporisée ou enrichie de métadonnées.
Ces modèles posent surtout problème en concurrence. Un état global modifiable est partagé entre goroutines, impose des verrous, augmente le risque de courses critiques et rend le comportement dépendant de l’ordre d’exécution. Surtout, ils ne respectent pas la granularité du web : chaque requête doit pouvoir interrompre ses sous‑travaux sans impacter les autres. Un singleton ne sait pas à quel « client » un travail appartient ; Context porte précisément cette notion d’appartenance et transporte le signal d’annulation de la requête jusqu’aux couches profondes.
Le cycle de vie est un autre angle mort. Une variable globale vit aussi longtemps que le processus ; elle peut retenir des références qui empêchent le script de libérer des ressources, ou continuer à exécuter du travail alors que la requête d’origine est terminée. Un Context a, lui, un début et une fin claire : il est créé en entrée (souvent à partir de r.Context()), il se propage dans l’appel, puis il se termine via annulation, délai ou échéance. Cette borne temporelle est la clé pour éviter les fuites et les jobs orphelins.
Sur le plan de l’observabilité, les globaux dispersent l’information. Un trace ID placé en global peut « baver » d’une requête à l’autre si l’on oublie de le réinitialiser, et un timeout fixé dans une configuration globale n’est pas synchronisé avec les contraintes imposées par l’appelant. Avec Context, les métadonnées comme le trace ID, la locale ou l’identifiant utilisateur voyagent avec l’appel ; les timeouts et deadlines s’alignent par propagation, ce qui garantit une vision cohérente de bout en bout.
Enfin, pour les tests, les globaux transforment chaque scénario en casse‑tête d’initialisation et de nettoyage. Les tests deviennent interdépendants, difficiles à paralléliser et fragiles. Une API pilotée par Context est plus composable : chaque test crée son propre context.WithTimeout ou context.WithCancel, injecte ce contexte au point d’entrée et vérifie que l’annulation coupe bien les goroutines et libère les timers. On gagne en déterminisme et en vitesse d’exécution.
La voie idiomatique en Go consiste à combiner deux idées simples : utiliser des structures pour l’injection de dépendances longues (connexion à la base, clients externes, logger) et accepter un ctx context.Context en premier paramètre de chaque opération qui peut bloquer ou lancer des goroutines. On évite de stocker le Context dans les structs, on ne l’utilise pas pour transporter des paramètres métier, et l’on s’appuie sur lui uniquement pour la gestion du cycle de vie et des métadonnées request‑scoped. Ce duo injection de dépendances + Context remplace avantageusement les variables globales et les singletons, tout en restant prévisible et testable.
Quand ne pas utiliser Context ?
Le Context n’est pas un conteneur générique destiné à transporter tout et n’importe quoi. Lorsqu’une fonction a besoin de paramètres métier comme une limite de pagination, un critère de tri, un filtre ou un corps de requête, ces informations doivent figurer dans la signature de la fonction à côté des autres arguments explicites. Mettre ces données dans le Context les rend invisibles pour le lecteur, fragilise l’autocomplétion et complique la documentation ; on perd le contrat d’usage qu’exprime normalement une signature claire. Le Context doit rester réservé aux signaux transverses (annulation, deadline) et à des métadonnées légères, strictement liées au cycle de vie d’une requête.
Il ne faut pas non plus s’en servir pour véhiculer des objets volumineux ou des caches. Un Context est éphémère, souvent copié et transmis de goroutine en goroutine ; y loger des structures lourdes exerce une pression inutile sur le ramasse‑miettes et brouille la responsabilité de ces données. Un cache vit à l’échelle d’un processus, d’un composant ou d’un sous‑système, pas à l’échelle d’une requête ; il mérite une dépendance dédiée (par exemple un champ dans une struct) avec une politique de concurrence claire.
Enfin, le Context n’est pas un mécanisme de partage d’état mutable entre goroutines. S’il faut coordonner des écritures, signaler la disponibilité d’un résultat ou agréger des erreurs, on utilisera la concurrence de Go : channels, sync.Mutex, sync.Map, sync/atomic, ou des helpers comme errgroup. Le Context émet un signal d’annulation et fournit une échéance ; il ne doit pas être détourné en bus de données ou en registre partagé.
Erreurs classiques et comment les éviter
La première erreur consiste à publier des APIs publiques qui ne prennent pas ctx alors que l’opération peut bloquer ou dépend d’une ressource externe. On se condamne alors à bricoler plus tard une variante « WithContext », ce qui fracture l’API et multiplie les chemins de code. Mieux vaut l’assumer dès la conception : ctx en premier, paramètres métier ensuite.
Une autre faute courante est d’oublier d’appeler la fonction cancel() issue de WithTimeout, WithDeadline ou WithCancel. Même si le délai est atteint, l’appel de cancel libère les timers internes et évite de petites fuites chroniques qui, sous charge, finissent par peser lourd. La règle pratique est simple : créer, puis immédiatement defer cancel().
Beaucoup de code finit par laisser traîner context.TODO() en production. Ce marqueur a son utilité pendant un refactoring, mais il doit être remplacé par un contexte parent réel, généralement celui de la requête (r.Context() côté HTTP). Conserver des TODO revient à ignorer l’annulation, à perdre la cohérence des délais et à casser l’observabilité bout‑en‑bout.
Il est également fréquent de multiplier les timeouts contradictoires à différents niveaux de la pile. On impose deux secondes au middleware HTTP, on ajoute une seconde dans la couche service et on redemande cinq cents millisecondes au niveau du repository ; le résultat est imprévisible et difficile à diagnostiquer. La bonne approche est de fixer une contrainte à la frontière puis de propager ce même contexte vers l’aval, en ne resserrant localement que lorsqu’une dépendance particulière l’exige vraiment.
Stocker un Context dans une struct est un piège discret mais coûteux. Un Context représente un instantané du cycle de vie d’une requête ; le conserver au‑delà de cette requête entraîne des confusions, voire la réutilisation accidentelle d’un contexte annulé. Il faut au contraire le faire entrer par paramètre à chaque opération et ne jamais l’attacher à un type longue durée. De la même manière, recréer un context.Background() au milieu de l’appel pour « repartir propre » débranche l’annulation et la deadline d’origine ; on perd le fil conducteur et l’application devient incohérente face à la pression.
Dernier travers, oublier d’écouter Done() dans les goroutines filles. Une goroutine qui ne réagit pas au signal d’annulation continue à exécuter du travail que personne n’attend plus, retient des connexions ouvertes et empêche l’arrêt propre d’un serveur. Toute boucle qui lit, attend ou calcule doit régulièrement vérifier le contexte et renvoyer rapidement ctx.Err() lorsque c’est demandé.
Conclusion
Maîtriser context en Go, c’est accepter que le code vit dans un monde imparfait : latences, interruptions, fermetures, dépendances capricieuses. Le paquet context offre un contrat prévisible, composable et uniforme pour faire face à ces réalités : on propage un signal d’annulation clair, on borne la durée des opérations, on transmet des métadonnées proprement en s’intégrant naturellement avec l’écosystème Go.
Context ne remplace ni des arguments métiers bien nommés, ni un système de cache, ni les primitives de concurrence. Il offre un langage commun pour exprimer l’annulation, les délais et de petites métadonnées liées à une requête. En l’utilisant à bon escient ctx en premier, pas de stockage persistant, pas d’objets lourds, pas de paramètres métier. On obtient des APIs prévisibles, des services web qui se ferment proprement et une observabilité cohérente de bout en bout.