context Values: When and When Not
context.WithValue carries request-scoped metadata down call stacks without threading ten string parameters through every function.
Search across all documentation pages
context.WithValue carries request-scoped metadata down call stacks without threading ten string parameters through every function.
Used well, values hold trace IDs and auth principals; used poorly, they become a global map for optional business data.
Store cross-cutting infrastructure metadata in context with unexported typed keys.
Pass business inputs as explicit function parameters.
Never put secrets in context values without understanding they can leak through logging and introspection.
Quick-reference recipe card - copy-paste ready.
type ctxKey int
const (
requestIDKey ctxKey = iota
userKey
)
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
v, _ := ctx.Value(requestIDKey).(string)
return v
}When to reach for this:
package main
import (
"context"
"fmt"
"log/slog"
)
type ctxKey string
const (
requestIDKey ctxKey = "requestID"
userKey ctxKey = "user"
)
type User struct {
ID string
Role string
}
func withMeta(ctx context.Context, reqID string, u User) context.Context {
ctx = context.WithValue(ctx, requestIDKey, reqID)
return context.WithValue(ctx, userKey, u)
}
func logAction(ctx context.Context, action string) {
attrs := []any{slog.String("action", action)}
if id, ok := ctx.Value(requestIDKey).(string); ok && id != "" {
attrs = append(attrs, slog.String("request_id", id))
}
if u, ok := ctx.Value(userKey).(User); ok && u.ID != "" {
attrs = append(attrs, slog.String("user_id", u.ID))
}
slog.Info("audit", attrs...)
}
func serviceCall(ctx context.Context) {
logAction(ctx, "billing.sync")
}
func main() {
ctx := withMeta(context.Background(), "req-7f3a", User{ID: "u-42", Role: "admin"})
serviceCall(ctx)
fmt.Println("done")
}What this demonstrates:
Value walks parent pointers until a key matches.WithValue calls shadow parent keys with the same type and value.WithValue.| Acceptable | Unacceptable |
|---|---|
| Request / trace ID | Database handles |
| Authenticated principal snapshot | Large request payloads |
| Locale or tenant slug | Optional query filters |
| Logger or tracer handle (carefully) | Configuration structs |
| Deadline policy flags (rare) | Secrets in plain text |
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := UserFromJWT(r.Header.Get("Authorization"))
ctx := context.WithValue(r.Context(), userKey, u)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Prefer small value types copied by value or immutable pointers.
// Package auth owns its keys - other packages cannot collide
package auth
type key struct{}
var userKey = key{}
func User(ctx context.Context) (User, bool) {
u, ok := ctx.Value(userKey).(User)
return u, ok
}"user". Fix: unexported typed keys per package.*sql.DB via constructors, not context.WithValue(ctx, key, nil) still stores an entry. Fix: omit key when absent or use pointer types.| Alternative | Use When | Don't Use When |
|---|---|---|
| Explicit struct parameter | Clear API with few fields | Deep middleware-only metadata |
context.Value | Cross-cutting IDs across packages | Business logic inputs |
| Thread-local style (not in Go) | N/A in Go | - |
| OpenTelemetry baggage | Standard tracing propagation | Simple monolith without tracing |
| HTTP headers re-read at leaf | Stateless microservices | Hot paths calling same metadata repeatedly |
If you need more than a handful, reconsider an explicit request struct.
Context values should stay tiny and stable.
Some teams do; others inject loggers via constructors.
If stored, use a narrow interface and avoid mutable state.
Yes - use helper constructors in _test.go to attach test metadata.
Keeps production key types unexported.
They are not encrypted or access-controlled.
Do not treat context as a secrets vault.
gRPC metadata is wire-level; context values are in-process.
Interceptors often copy metadata into context for handlers.
Not automatically - serialize chosen fields into headers or metadata explicitly.
Recreate values in receiving middleware.
Cancellation and deadlines are first-class context features.
Values are ancillary - do not use them to signal stop.
Typed helper functions (User(ctx)) are idiomatic.
Generics rarely simplify beyond plain functions.
No - child contexts inherit parent cancellation.
Values add a layer without changing Done behavior.
golangci-lint and review checklists flag string keys and context-as-config patterns.
Enforce package-owned key types in style guides.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026