Vol. 8 — № 03
The Go Loop · KiosqueNewsstand
Blog  
Un atelier Go · Édition d'ApprentissageA Go Workshop · Learning Edition

La valeur dans le contexte (et pourquoi s'en méfier) The value in the context (and why to distrust it)

WithValue glisse une donnée dans le contexte — pratique, et dangereux. Clés typées, jamais une string nue ; un request ID y a sa place, une dépendance non. Le contexte n'est pas un sac à injection. WithValue slips a datum into the context — handy, and dangerous. Typed keys, never a bare string; a request ID belongs there, a dependency doesn't. The context is not an injection bag.

AudienceAudience
Dev qui stocke son logger ou sa DB dans le contexte, ou qui utilise des strings comme clés de WithValue Dev who stores their logger or DB in context, or uses strings as WithValue keys
Format
Self-paced
ChapitresChapters
5
Date
Mai 2028 May 2028
≈ 15 min ●●●○ contextValuesAnti-pattern

Chapitre 1 en accès libre — la suite (ch. 2 à 5) est réservée. Chapter 1 free to read — the rest (ch. 2–5) is members-only.

01CadrageFraming3 min

WithValue glisse une donnée dans le contexte — clé typée, jamais une string. Un request ID y a sa place ; une dépendance non.WithValue slips data into the context — typed key, never a string. A request ID belongs there; a dependency doesn't.

Les deux premiers numéros du volume 8 ont couvert le signal dans le contexte — l'annulation et le temps. context.WithValue ajoute une troisième dimension : la donnée. WithValue prend un contexte parent, une clé, et une valeur, et retourne un nouveau contexte qui les mémorise. La valeur est lisible en aval par ctx.Value(clé) — sans modifier les signatures des fonctions intermédiaires. C'est cette propriété qui rend WithValue tentant : plus besoin de passer un request ID à chaque couche, on le glisse dans le contexte au niveau du middleware et on le lit où on en a besoin. Mais cette même propriété est aussi son danger : elle rend les dépendances implicites, invisibles à la compilation, et difficiles à tracer. Pour choisir la bonne clé, la règle est nette : jamais une string nue, toujours un type défini dans le package qui l'utilise. La raison tient en un mot : collision. Si deux packages utilisent la chaîne "user" comme clé, ctx.Value("user") retourne le premier ou le dernier selon l'ordre d'écriture — sans avertissement, sans erreur. Un type privé non-exporté est, par définition, unique à son package : deux types contextKey définis dans deux packages différents sont deux types distincts en Go, même s'ils ont le même nom. C'est exactement ce qu'on veut.The first two volume 8 issues covered the signal in the context — cancellation and time. context.WithValue adds a third dimension: data. WithValue takes a parent context, a key, and a value, and returns a new context that stores them. The value is readable downstream via ctx.Value(key) — without modifying intermediate function signatures. It's this property that makes WithValue tempting: no more passing a request ID through every layer, you slip it into the context at middleware level and read it where needed. But that same property is also its danger: it makes dependencies implicit, invisible to the compiler, and hard to trace. For choosing the right key, the rule is sharp: never a bare string, always a type defined in the package that uses it. The reason fits in one word: collision. If two packages use the string "user" as a key, ctx.Value("user") returns the first or last depending on write order — without warning, without error. A private unexported type is, by definition, unique to its package: two contextKey types defined in two different packages are two distinct types in Go, even if they have the same name. That's exactly what you want.

WithValue avec clé typée — lire et écrire sans collisionWithValue with typed key — read and write without collision
// Un type de clé privé — jamais une string nue.
type contextKey int
const requestIDKey contextKey = iota

// Écrire dans le contexte
func withRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

// Lire depuis le contexte — avec une assertion de type sûre
func requestIDFrom(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(requestIDKey).(string)
    return id, ok
}

// Dans un middleware HTTP :
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" { id = uuid.New().String() }
        ctx := withRequestID(r.Context(), id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
Ce qui appartient au contexteWhat belongs in context
request ID
✓ appartient ici✓ belongs here
trace ID
✓ appartient ici✓ belongs here
user ID authentifié
✓ appartient ici✓ belongs here
logger
✗ passe explicitement✗ pass explicitly
db *sql.DB
✗ passe explicitement✗ pass explicitly
config
✗ passe explicitement✗ pass explicitly

Le contexte porte des métadonnées de la requête, pas des dépendances du programme. La règle : « est-ce que cette valeur change d'une requête à l'autre ? » Si oui, le contexte est le bon endroit. Sinon, passe-la explicitement.Context carries request metadata, not program dependencies. The rule: "does this value change from one request to the next?" If yes, context is the right place. If not, pass it explicitly.

Le réflexe du numéroThe issue's reflex

« Cette valeur est-elle une métadonnée de la requête — un ID, un token, une trace — ou une dépendance du programme — un logger, une base, un client ? » Si c'est une métadonnée : WithValue avec une clé typée, un getter, un setter. Si c'est une dépendance : passe-la explicitement dans les constructeurs ou les arguments. Ce test simple évite 90 % des abus de context.Value dans une base de code."Is this value a request metadata item — an ID, a token, a trace — or a program dependency — a logger, a database, a client?" If it's metadata: WithValue with a typed key, a getter, a setter. If it's a dependency: pass it explicitly in constructors or arguments. This simple test avoids 90% of context.Value abuses in a codebase.

context.Value est O(n)context.Value is O(n)

Chaque appel à context.WithValue crée un nouveau nœud dans la chaîne de contextes. ctx.Value(key) remonte cette chaîne nœud par nœud jusqu'à trouver la clé. Dans une chaîne courte — deux ou trois valeurs — c'est négligeable. Mais si on utilise le contexte comme un sac général et qu'on y empile des dizaines de valeurs, la recherche devient linéaire dans la profondeur de la chaîne. C'est une raison de plus de n'y mettre que ce qui y a vraiment sa place — un nombre raisonnable de métadonnées de requête, pas toutes les dépendances du service.Each call to context.WithValue creates a new node in the context chain. ctx.Value(key) climbs this chain node by node until it finds the key. In a short chain — two or three values — it's negligible. But if you use context as a general bag and pile dozens of values into it, the lookup becomes linear in the chain's depth. It's one more reason to put only what truly belongs there — a reasonable number of request metadata items, not all service dependencies.

🔒

La suite est réservée The rest is members-only

Le premier numéro est libre. Débloque tout The Go Loop — tous les volumes, à vie — pour 5 €, paiement unique. The first issue is free. Unlock all of The Go Loop — every volume, forever — for €5, one-time.

Retour au kiosqueBack to newsstand