How does ctx.Value locate a value, and what does that cost as the context chain grows?
answer
- a chain, not a map
- each With call adds exactly one node
- a miss walks all the way to the root
- nearest node wins; the parent is unchanged
- linear in chain depth, per lookup
basics
~20 sEach context.WithValue call wraps the parent in a new node holding one key and value. Value checks its own key, then asks its parent — so a lookup is a linear walk up the chain, and a miss reaches the root.
solid answer
~40 sA context built with `context.WithValue` is an immutable linked node: it stores one key, one value and a pointer to its parent. `Value` compares the requested key against its own, and on a mismatch delegates upward, so a lookup costs one interface comparison per node between the leaf and the node that holds the key — and a miss walks the whole chain, past cancellation nodes too, before returning `nil`. Two consequences follow. Storing the same key twice does not replace anything: the nearer node shadows the older one for its descendants, while the parent still returns its own value. And a lookup is O(depth), so keep the chain to a handful of values and read what you need once at the boundary into a local variable rather than calling `Value` inside a loop.
code
go · 9 linesfunc shadowing() {
type userKey struct{}
ctx := context.WithValue(context.Background(), userKey{}, "alice")
child := context.WithValue(ctx, userKey{}, "bob")
fmt.Println(child.Value(userKey{})) // bob: the nearest node answers
fmt.Println(ctx.Value(userKey{})) // alice: the parent is unchanged
}go deeper
Remember the shape: every WithValue call makes a new context wrapping the old one, and the old one keeps working exactly as before. Nothing is ever set or overwritten in place.
Be ready to describe the walk precisely — one key per node, compare then delegate to the parent, root returns nil — and to draw the conclusion that a lookup is linear in chain depth and a miss is the most expensive case.
Show where the cost becomes real: a deep chain queried inside a loop over many items, versus one read at the request boundary. Be able to argue that the loss of compile-time checking matters more than the chain walk.
Set the expectation for how many values a request chain may carry and who may add one, so no team turns the chain into an ad-hoc store that every downstream package quietly reads from.
## The data structure A `context.Context` is not a bag or a map. It is a **tree of immutable nodes**, each holding a pointer to its parent. `context.Background()` is a root. Every `With...` call returns a new child node; the parent it was given is unchanged and still usable. The node that `context.WithValue` returns holds exactly **one** key/value pair. Its `Value` method is essentially: - if the requested key equals my key, return my value; - otherwise, ask my parent the same question. The root returns `nil` for anything, so a lookup that finds nothing costs a full walk to the root and yields an untyped `nil` interface. ## Consequences of immutability Because nothing is mutated, `ctx = context.WithValue(ctx, k, v)` is not "setting a field": it is building a new node and rebinding your variable. Anyone still holding the old context sees the old view. That is what makes contexts safe to share: many goroutines may call `Value` on the same context concurrently, because the nodes are written once at derivation and never change afterwards. (The *value stored inside* is a different matter — put a mutable map in there and you have shared mutable state again.) It also means **shadowing rather than replacement**. Store key `k` twice in one chain and the nearer node wins for everything derived from it, while the outer node keeps answering with the older value for anything derived from *it*. That is occasionally useful — a nested scope overriding a value for its own subtree — and occasionally a surprise, when someone expects a later write to be visible to code holding an earlier context. ## What the lookup costs Per node the work is tiny: an interface comparison and a pointer hop. But the shape is a **linear scan**, not a hash lookup, and the chain contains every node in the derivation, including cancellation and deadline nodes, which simply forward the call upward. So the cost model is: - **hit** — proportional to the distance from the leaf down to the node holding the key; - **miss** — proportional to the full depth of the chain, every time. With three or four values in a chain this is irrelevant. It stops being irrelevant when a request accumulates a dozen values and inner code calls `Value` on a per-item basis inside a loop over thousands of records — then you are paying a chain walk per item to fetch something that never changes for the whole request. There is a second, smaller cost: the value is stored as `any`, so a non-pointer-shaped value has to be boxed, which can allocate at the point where you call `WithValue`. ## How to use it well - **Keep the chain short.** A handful of request-scoped values, not a general-purpose store. - **Read once at the boundary.** Pull the value out where the request enters your code, into a local variable, and pass it explicitly to the code below. This is both faster and clearer than repeated implicit lookups. - **Do not treat a repeated key as an update.** If your design needs a value to change over the life of the request, a context value is the wrong mechanism — every derived context that already exists keeps the old view. - **Do not reach for `Value` in a hot loop.** The cost is small but it is paid per call, and the readability cost is paid by every reader. ## The real cost is not the walk Even where the walk is free, every value moved into the chain is a dependency the compiler cannot see. The lookup gives you `any` and a boolean at best, so what was a checked parameter becomes a runtime question. That is the reason to keep the chain small, far more than the nanoseconds.
- If two nodes in the same chain hold the same key, which one answers?The one nearest the context you call `Value` on. The walk stops at the first match going upward, so the inner node shadows the outer one for everything derived from it. The outer context is untouched and still returns its own value to anything holding it — nothing was replaced or removed.
- Is it safe for many goroutines to call ctx.Value on the same context at once?Yes. Contexts are immutable — the key and value are fixed when the node is created — and the standard library documents a Context as safe for simultaneous use by multiple goroutines. The hazard is what you put inside: store a mutable map or a slice you later write to, and every reader shares it, with no synchronisation supplied by the context.
- How would you avoid repeated ctx.Value calls in a hot code path?Fetch the value once where the request enters your code, keep it in a local variable, and pass it explicitly to the functions below. That turns a chain walk per iteration into a single lookup, and it makes the dependency visible in the signatures instead of hidden in a lookup that may or may not find anything.
The values are notes pinned to nested envelopes. You read the note on the innermost envelope, then the one it sits inside, and so on until you find your key or run out of envelopes.
saying these in an interview costs you the question
- Describes ctx.Value as a map or dictionary lookup
- Thinks context.WithValue mutates the parent context
- Assumes storing a key twice replaces the earlier value everywhere
- Stacks a dozen values and calls Value inside a per-item loop
- Stores a mutable shared map as a context value and calls it safe