Golang: Context
Go’s context package manages process lifecycles, handles timeouts & cancellations and propagates request-scoped metadata across API boundaries and concurrent goroutines. It acts as a command signal passed along a chain of workers (goroutines) that allows a parent process to tell everyone downstream to stop working immediately when they are no longer needed. Cancellation is cooperative: context only sends the signal, so your code has to check Done() and return.
Four Pillars
Done()- Returns a read-only channel (<-chan struct{}) that closes when work should stop.- Usage: listening in a
selectstatement to halt loops or database queries.
- Usage: listening in a
Err()- Returns anerrorexplaining why the channel closed:context.Canceledorcontext.DeadlineExceeded.- Usage: checking why an operation failed or logging the termination reason.
Deadline()- Returns the exact time (time.Time) this context will be automatically canceled, plus anokbool that isfalseif there is no deadline.- Usage: checking if there is enough time left to begin an expensive step.
Value()- Fetches an immutable, request-scoped value associated with a specific key. It returnsany, so callers type-assert the result.- Usage: reading request data like a trace ID or the authenticated user.
Creating Contexts
context.Background()- The default empty root context. Use this at the highest level of your application or the entry point of a request handler.context.TODO()- Placeholder context.context.WithCancel(Parent)- Returns a copy of the parent along with acancel()function. Invokingcancel()manually triggers the closure of the child’sDone()channel.context.WithTimeout(Parent, Duration)- Automatically cancels the context after a set timeframe passes.context.WithDeadline(Parent, Time)- Automatically cancels the context at a specifictime.Time.context.WithValue(Parent, Key, Value)- Attaches custom data to the context.
Example
work runs a step every 300ms until its context times out after one second.
func work(ctx context.Context) error {
for step := 1; ; step++ {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(300 * time.Millisecond):
fmt.Println("finished step", step)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := work(ctx); err != nil {
fmt.Println(err) // after 3 steps: context deadline exceeded
}
} Golden Rules
- Pass it First - Always make
ctx context.Contextthe very first parameter of a function. - Never Store in Structs - Pass context explicitly through call stacks rather than keeping it inside an object instance.
- Always Defer Cancel - When using
WithCancel,WithTimeoutorWithDeadline, always immediately rundefer cancel()to release the context’s resources (its timer and its link to the parent) as soon as the function returns. - Keep Values Lightweight - Don’t use
WithValueto pass database connections or optional parameters. Use it strictly for request-scoped variables like user tracking or trace IDs, and make keys an unexported custom type so they don’t collide with keys from other packages.