context.Context
Q: What problem does context.Context solve, and what doesn't it solve?
Problem: in a server, one incoming request spawns multiple goroutines (DB query, RPC call, background work). When the client disconnects or a deadline expires, you need all those goroutines to stop — but Go has no built-in way to "kill" a goroutine. context.Context provides a shared cancellation signal and deadline that flows across API and goroutine boundaries without global variables.
What it solves: propagation of cancellation, deadlines, and request-scoped values (trace IDs, auth tokens) through call chains.
What it doesn't solve: cancellation is cooperative. If a callee never checks ctx.Done(), cancellation does nothing. A goroutine blocked in a pure compute loop or a non-ctx-aware library won't be interrupted. The context signal is advisory — the callee must opt in.
Q: What are the context constructors and when do you use each?
| Constructor | Use case |
|---|---|
context.Background() | Top-level root: main, init, tests, top of a server handler |
context.TODO() | Placeholder when you're unsure which context to use — a code smell if it stays long-term |
WithCancel(parent) | Manual cancellation; returns ctx, cancel |
WithTimeout(parent, d) | Cancel after duration from now |
WithDeadline(parent, t) | Cancel at an absolute time |
WithValue(parent, key, val) | Attach request-scoped data |
All derived contexts form a tree: cancelling a parent cancels all children. WithTimeout/WithDeadline also cancel all children when the time expires.
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel() // always call to release resources even if timeout fires firstQ: What are the rules for using context correctly?
- Pass as the first argument:
func DoWork(ctx context.Context, ...)— convention enforced bygo vetand linters. - Never store in a struct field (except for backward-compat wrappers). Storing makes the lifetime opaque; passing makes it explicit at every call site.
- Always
defer cancel()right afterWithCancel/WithTimeout/WithDeadline. Even when the timeout fires, the cancel function releases the timer goroutine and the context's internal resources. Forgetting it leaks a goroutine until the parent is cancelled. WithValueonly for request-scoped data with unexported typed keys — never for optional function parameters.
type ctxKey string
const traceIDKey ctxKey = "traceID"
ctx = context.WithValue(ctx, traceIDKey, "abc-123")
id, _ := ctx.Value(traceIDKey).(string)Using an unexported type prevents key collisions between packages.
Q: How do you correctly select on ctx.Done() alongside real work?
func process(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
case j, ok := <-jobs:
if !ok {
return nil // channel closed, done
}
if err := doWork(ctx, j); err != nil {
return err
}
}
}
}Key points:
- Check
ctx.Err()to distinguishCanceled(explicit cancel) fromDeadlineExceeded(timer fired) — useful for metrics/logging. - Pass
ctxinto every sub-call so they can short-circuit too. - If
doWorkis synchronous and long, it must also select onctx.Done()internally or it won't respect cancellation.
Q: What are the common context pitfalls and how do you detect them?
Leak from missing cancel: WithTimeout spawns a timer goroutine; forgetting defer cancel() leaks it until the parent context ends. Use go.uber.org/goleak in tests to catch this. See Goroutine Leaks.
Ignoring ctx in libraries: wrapping a library that doesn't accept a context means you can't cancel its work. Workaround: run it in a goroutine, select on ctx.Done() and kill/abandon the result on cancellation (while being careful about cleanup).
Over-using WithValue: values stored in context are interface{} — no type safety at compile time, and retrieval is a linear walk up the context tree. Don't put large structs or optional function parameters in context; pass them as explicit arguments.
Passing a cancelled context to a new request: after a handler returns, its context is cancelled. Creating background work with it will fail immediately. Use context.WithoutCancel(ctx) (Go 1.21+) or context.Background() for background jobs that should outlive the request.
Common follow-ups
- How do you propagate trace IDs from an incoming HTTP request through all downstream calls?
- What's the difference between
context.Canceledandcontext.DeadlineExceededat the call site? - How does
context.WithoutCancel(Go 1.21) differ from usingcontext.Background()for fire-and-forget goroutines? - How do you unit-test code that checks
ctx.Done()without real timers?