Context¶
A context.Context carries cancellation, deadlines, and request-scoped
values across API boundaries and goroutines. It's how you tell a tree of
goroutines "stop now" — when a request is cancelled, a timeout fires, or a
server is shutting down.
The core idea: a Context exposes a Done() channel that closes when
the context is cancelled. Goroutines select on it and bail out.
Roots: Background and TODO¶
Every context tree starts from a root. context.Background() is the usual
one (top of main, incoming requests). context.TODO() is a placeholder
for "I haven't wired context through here yet."
You never cancel the root directly — instead you derive a child context that can be cancelled.
WithCancel: explicit cancellation¶
context.WithCancel returns a child context and a cancel function.
Calling cancel closes the context's Done() channel, which every
goroutine watching it observes.
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
<-ctx.Done() // blocks until cancelled
fmt.Println("worker:", ctx.Err()) // worker: context canceled
close(done)
}()
cancel() // trigger cancellation
<-done
// output:
// worker: context canceled
ctx.Err() reports why it ended: context.Canceled after cancel, or
context.DeadlineExceeded after a timeout. Always call cancel
(typically defer cancel()) to release resources, even if the work
finished normally.
WithTimeout and WithDeadline¶
WithTimeout cancels automatically after a duration; WithDeadline at a
fixed time. Combine with select to bound any blocking operation:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
select {
case <-time.After(time.Second):
fmt.Println("work finished")
case <-ctx.Done():
fmt.Println("gave up:", ctx.Err()) // gave up: context deadline exceeded
}
The 10 ms timeout fires long before the 1 s work, so ctx.Done() wins and
ctx.Err() is context.DeadlineExceeded.
Propagation: pass it down, don't store it¶
The conventions are firm and worth following exactly:
- Pass
ctxas the first parameter, namedctx:func Fetch(ctx context.Context, url string) (...). - Don't store a
Contextin a struct — thread it through calls. - Derive child contexts as work fans out; cancelling a parent cancels all its children.
- A function that respects context selects on
ctx.Done()in its blocking loops and returnsctx.Err()when it fires.
func work(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // stop promptly when cancelled
default:
// ... one unit of work ...
return nil
}
}
}
Request-scoped values (use sparingly)¶
context.WithValue attaches a key/value pair that travels with the
context — meant for request-scoped metadata like a request ID, not for
passing optional function arguments. Overusing it hides dependencies, so
prefer explicit parameters and reach for values only for cross-cutting
data.
Use an unexported custom key type, not a bare string, so keys from different packages can't collide:
type ctxKey string
ctx := context.WithValue(context.Background(), ctxKey("reqID"), "abc123")
fmt.Println(ctx.Value(ctxKey("reqID"))) // output: abc123
fmt.Println(ctx.Value(ctxKey("missing"))) // output: <nil>
Value returns any, so it's nil for an absent key and you usually
type-assert the result back to its concrete type before using it.
Quick reference¶
| Call | Meaning |
|---|---|
context.Background() |
root context |
context.TODO() |
placeholder root |
ctx, cancel := WithCancel(parent) |
manual cancellation |
WithTimeout(parent, d) |
auto-cancel after duration |
WithDeadline(parent, t) |
auto-cancel at a time |
<-ctx.Done() |
closed when cancelled |
ctx.Err() |
Canceled or DeadlineExceeded |
WithValue(parent, k, v) |
request-scoped data (sparingly) |
first arg ctx context.Context |
the convention |