Cancellation, deadlines, and request-scoped values.
Cancellation & Deadlines with context
The context package is how Go programs propagate cancellation, deadlines,
and request-scoped values across goroutines and API boundaries. Every standard
library function that does I/O over the network takes a context.Context as
its first argument — and you should too.
The two roots
ctx := context.Background() // top of a long-running process
ctx := context.TODO() // placeholder while you decide what to use
Then you derive a new context with a deadline or a cancel function:
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
ctx, cancel := context.WithDeadline(parent, t)
defer cancel()
You must always call cancel() (usually via defer) — even if the context
finished naturally. That releases resources held by the context.
Cancellation
ctx.Done() returns a channel that is closed when the context is cancelled or
exceeded. Long-running goroutines should poll it via select.
Passing context everywhere
The convention is to pass ctx as the first argument, named ctx:
func FetchUser(ctx context.Context, id int) (*User, error) { ... }
Never store a context in a struct field — pass it through. Never pass nil
for a context; use context.TODO() if you really don't have one.
Request-scoped values (with care)
You can also attach values to a context — typically things like a request ID or a per-request logger:
ctx = context.WithValue(parent, requestIDKey, reqID)
v := ctx.Value(requestIDKey)
Use unexported key types for context.Value
To prevent collisions between packages, define a private key type:
package mypkg
type ctxKey int
const requestIDKey ctxKey = 0
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
s, _ := ctx.Value(requestIDKey).(string)
return s
}
Using a bare string as the key would let other packages accidentally overwrite
each other's values.
Checking why a context ended
err := ctx.Err()
// nil — still active
// context.Canceled — somebody called cancel()
// context.DeadlineExceeded — the deadline passed
Use errors.Is(err, context.DeadlineExceeded) when you want to distinguish a
timeout from other failures (e.g., to decide whether to retry).