Go Context
context.Context controls cancellation, deadlines, and request-scoped values
across API boundaries. Misusing it causes goroutine leaks, orphaned work,
and subtle production bugs.
Community default. A project skill or agent-config rule that explicitly supersedes
go-contexttakes precedence over this one.
Detailed reference material, loaded on demand:
references/timeout-budgets.md— dividing a parent's remaining budget, readingctx.Deadline().references/values.md— context key types, accessors, collision traps.references/http-and-testing.md— request context in handlers and middleware, and cancellation tests.references/cause-and-detach.md—WithCancelCause,WithTimeoutCause,WithoutCancel,AfterFunc.
Read a reference file only when the section below is not enough.
1. Core Rules
Context is always the first parameter:
go// ✅ Good — context is first func GetUser(ctx context.Context, id string) (*User, error) func (s *Service) Process(ctx context.Context, req Request) error // ❌ Bad — context buried in the middle or end func GetUser(id string, ctx context.Context) (*User, error) func Process(req Request, ctx context.Context) error
NEVER store context in a struct:
go// ❌ Bad — context stored in struct type Server struct { ctx context.Context // NEVER do this cancel context.CancelFunc } // ✅ Good — pass context through method parameters func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) }
Context represents the lifetime of a single operation, not the lifetime of an object.
NEVER pass nil context:
go// ❌ Bad doSomething(nil, data) // ✅ Good — use context.TODO() if unsure which context to use doSomething(context.TODO(), data) // ✅ Good — use context.Background() for top-level/main doSomething(context.Background(), data)
2. Cancellation
Always defer cancel:
go// ✅ Good — cancel called even if operation succeeds ctx, cancel := context.WithCancel(parentCtx) defer cancel() result, err := longOperation(ctx)
Failing to call cancel leaks resources (timers, goroutines) until the parent context is cancelled.
Use WithCancel for manual cancellation:
gofunc (s *Supervisor) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return s.runWorkerA(ctx) }) g.Go(func() error { return s.runWorkerB(ctx) }) // If any worker returns an error, errgroup cancels ctx, // which signals all other workers to stop. return g.Wait() }
Check context cancellation in loops:
go// ✅ Good — respects cancellation func processItems(ctx context.Context, items []Item) error { for _, item := range items { if err := ctx.Err(); err != nil { return fmt.Errorf("processing cancelled: %w", err) } if err := process(ctx, item); err != nil { return fmt.Errorf("process item %s: %w", item.ID, err) } } return nil } // ❌ Bad — runs to completion even if cancelled func processItems(ctx context.Context, items []Item) error { for _, item := range items { process(ctx, item) // ignores ctx cancellation between items } return nil }
3. Timeouts and Deadlines
WithTimeout for duration-based limits:
gofunc (c *Client) FetchUser(ctx context.Context, id string) (*User, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/users/"+id, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch user %s: %w", id, err) } defer resp.Body.Close() // ... }
WithDeadline for absolute time limits:
go// Use when coordinating with external deadlines (SLAs, cron windows) deadline := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) ctx, cancel := context.WithDeadline(ctx, deadline) defer cancel()
Timeout budgets
A child timeout spends part of the parent's budget; it never extends it. A 5s DB call and a 10s API call inside a 30s request handler are a budget. A 60s child under a parent with 5s left silently fires at the parent's deadline, which reads as a bug at 3am.
Before starting work that cannot be interrupted, read the remaining budget
with ctx.Deadline() and fail fast when it is too short.
Examples in references/timeout-budgets.md.
4. Context Values
Store only request-scoped metadata: request ID, trace or span ID, the authenticated user, a request-scoped logger. Never a database connection, configuration, or anything the function could take as an explicit parameter.
Keys must be an unexported type, never a string — a string key can be
overwritten by any other package in the process. Export accessor functions
so callers never type-assert on ctx.Value themselves.
Examples in references/values.md.
5. Context in HTTP Handlers and Tests
Handlers take r.Context() and pass it downstream. A cancelled request means
the client disconnected: return without writing a response. Middleware
attaches values with r.WithContext(ctx).
Tests derive contexts from t.Context() (Go 1.24+), not
context.Background(), wrap the call under test in context.WithTimeout so
a hang fails the test instead of blocking the suite, and assert
errors.Is(err, context.Canceled) to prove cancellation is honoured.
Examples in references/http-and-testing.md.
6. Causes, Detached Work, and AfterFunc
| Need | API (Go version) |
|---|---|
| Record why work was cancelled | WithCancelCause + context.Cause (1.20) |
| Say which timeout fired | WithTimeoutCause, WithDeadlineCause (1.21) |
| Finish work after the request returns, keeping its values | WithoutCancel + your own timeout (1.21) |
| Unblock a call that takes no context | AfterFunc (1.21) |
Detached work always gets its own timeout. context.Background() would
also outlive the request, but it drops the trace ID. Examples:
references/cause-and-detach.md.
7. context.Background() vs context.TODO()
| Function | When to use |
|---|---|
context.Background() | Top-level: main(), init(). Intentional root context. Tests use t.Context() (Go 1.24+). |
context.TODO() | Placeholder when you don't know which context to use yet. Signals "this needs to be fixed". |
context.TODO() is a code smell in production code — replace it before shipping.
Verification Checklist
context.Contextis the first parameter in all functions that accept it- No context stored in struct fields
defer cancel()called immediately afterWithCancel,WithTimeout,WithDeadline- Long loops check
ctx.Err()between iterations - Child timeouts don't exceed parent timeout budget
- Context values use unexported key types with accessor functions
- Only request-scoped metadata stored in context values (not configs, connections)
- HTTP handlers use
r.Context()and pass it downstream - No
nilcontext passed — usecontext.TODO()orcontext.Background() - Tests use
context.WithTimeoutto prevent hanging - Work that outlives a request uses
WithoutCancelplus its own timeout, notcontext.Background()


