How is Value lookup implemented, and what are its performance implications?
Question 269HardGo 1.22 to 1.25
Every WithValue call wraps its parent in a valueCtx{Context; key, val any} that stores one pair. Value(k) is a linked-list walk: check this node's key, then move to the parent, until it reaches Background/TODO, which returns nil. Each lookup therefore costs O(depth), and a miss always walks the whole chain.
Implications:
- Deep middleware stacks that each add values (and cancel layers, which also forward
Value) make every lookup slower. Usually this doesn't matter, but it does in hot paths that read a value per operation. - Don't call
ctx.Valuein a tight loop. Read it once into a local variable. - To attach many related fields, store one struct pointer instead of many separate
WithValuelayers.
type reqMeta struct {
RequestID string
TenantID string
Start time.Time
}
type metaKey struct{}
func withMeta(ctx context.Context, m *reqMeta) context.Context {
return context.WithValue(ctx, metaKey{}, m) // one node, not three
}
Gotcha: values in context should be immutable or safe for concurrent use, because the same context is often shared across goroutines. Storing a pointer to a struct you later change without synchronization is a data race.
More on Context
- Q267Why should context keys use an unexported custom type instead of a string? Show the idiomatic pattern.
- Q268What does this print? (context value keys)
- Q270What belongs in context values and what doesn't?
- Q271Why is storing a context.Context in a struct considered an anti-pattern? Are there exceptions?
- Q272context.Background() vs context.TODO(): when do you use each, and why not pass nil?
- Q273What is context.AfterFunc and what are the semantics of its stop function?