Code Review: Go Context Misuse
Common Go context bugs: not passing context downstream, not checking ctx.Done() in loops, storing context in struct fields, ignoring cancel cleanup, calling cancel from inside the cancelled goroutine. Each one creates a goroutine leak or unresponsive cancellation.
What it is
Go's context.Context is the standard mechanism for cancellation, deadlines, and request-scoped values. Used correctly, it makes goroutine lifecycle predictable. Used incorrectly, it creates goroutine leaks, ignored timeouts, and unresponsive cancellation.
This lesson is a tour of the most common context bugs that show up in code review.
Bug 1: Not passing ctx down
The caller has a 5-second deadline. Calls a helper. Helper makes an HTTP request without using the ctx. The HTTP client uses its default timeout (often 30s). Caller's deadline fires; helper's HTTP call keeps running.
The helper's goroutine eventually completes, but the work was unnecessary. Worse, when many requests pile up like this, N goroutines are doing wasted work that the caller no longer needs.
Fix: thread ctx through every layer that does I/O. http.NewRequestWithContext, db.QueryRowContext, redis.GetWithContext, etc.
Bug 2: Storing ctx in a struct field
Looks innocuous: the struct has a few methods that all need ctx, so the developer puts it in a field at construction. Now every method uses s.ctx.
The trap: ctx is per-request. The first request's ctx gets stored. Subsequent requests use the same struct (singleton service); they inherit the original request's ctx, which has long since been cancelled. New requests see "context canceled" errors immediately.
Fix: pass ctx as a parameter on every method that needs it. Don't store.
Bug 3: Forgetting defer cancel
context.WithTimeout and context.WithCancel return a cancel function. It must be called (defer is the standard pattern). Skipping the call leaks the timer goroutine that manages the deadline.
Each leak is small (one goroutine, a timer), but at scale, they add up. vet and staticcheck catch many of these; rely on the linter.
Bug 4: Long loop without ctx check
Go's scheduler doesn't preempt goroutines based on context. A goroutine in a tight loop won't notice cancellation until it explicitly checks.
For loops that do work item-by-item:
if ctx.Err() != nil { return ctx.Err() }at the top of each iteration.
For loops that wait on channels:
selectwith bothctx.Done()and the channel.
For long pure-compute work:
- Periodic checks every N iterations, OR design to be interruptible.
Bug 5: context.Background in handlers
context.Background() is the root context: never cancelled, no deadline. It's appropriate at process startup or in tests.
In an HTTP handler, the request's r.Context() is the right choice. It's cancelled when the client disconnects. Using context.Background() instead means cancellation doesn't propagate; even after the client has hung up, downstream goroutines keep running.
Bug 6: ctx values for business data
context.WithValue is for request-scoped metadata: trace IDs, auth tokens, request IDs. It is not a way to pass business data through layers. Two reasons: it's untyped (callers must assert types at every read), and it makes function signatures lie ("looks like a function takes 2 args, actually depends on 5 ctx values").
Pass business data as explicit parameters. Use ctx values only for cross-cutting concerns.
Run go vet and staticcheck in CI. Both catch many context anti-patterns. Most context bugs are mechanical and a linter will find them.
Implementations
Key points
- •Always pass ctx as the FIRST parameter; don't store in struct fields.
- •Always defer cancel() after creating one with WithTimeout/WithCancel; otherwise leak.
- •Pass ctx down through every call that does I/O or might block.
- •In loops, check ctx.Done() either via select or ctx.Err().
- •Returning early from cancelled context is a goroutine's responsibility; runtime won't kill it.
Follow-up questions
▸Why is storing ctx in a struct an anti-pattern?
▸Should every function take ctx?
▸What's the difference between ctx.Done() and ctx.Err()?
▸Can a context be cancelled from inside the goroutine that received it?
Gotchas
- !Forgetting defer cancel() leaks the cancellation goroutine
- !Ignoring ctx in HTTP calls or DB queries: cancellation has no effect
- !Storing ctx in struct: subsequent calls inherit stale ctx
- !Using context.Background() in handlers: no inherited cancellation; should be the request's ctx
- !Putting business data in ctx values: anti-pattern; use explicit parameters