Go Context Package Cheat Sheet
Creating cancelable, timeout-bound, and value-carrying contexts with context.Context for propagating deadlines and cancellation across goroutines.
Creating Contexts
context.Context flows through call chains as the first parameter, conventionally named ctx.
ctx := context.Background() // root context, never canceled, use in main/testsctx2 := context.TODO() // placeholder when unsure which context to use yet// Derived, cancelable contextctx3, cancel := context.WithCancel(ctx)defer cancel() // always call cancel to release resources, even if ctx finishes normally
Deadlines & Timeouts
WithTimeout and WithDeadline auto-cancel the context after a duration or absolute time.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)resp, err := http.DefaultClient.Do(req)if err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Println("request timed out") } return err}// WithDeadline: cancel at a specific absolute timedeadline := time.Now().Add(10 * time.Second)ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)defer cancel2()
Respecting Cancellation in Goroutines
Long-running work must select on ctx.Done() to react to cancellation.
func worker(ctx context.Context, jobs <-chan int) { for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) // context.Canceled or DeadlineExceeded return case job, ok := <-jobs: if !ok { return } process(job) } }}
Passing Request-Scoped Values
context.WithValue for request-scoped data only — never for optional function params.
type ctxKey stringconst requestIDKey ctxKey = "requestID" // unexported custom type avoids collisionsfunc withRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id)}func requestID(ctx context.Context) (string, bool) { id, ok := ctx.Value(requestIDKey).(string) return id, ok}
Context API Reference
The full public surface of the context package.
- context.Background()- empty root context for main(), init, tests
- context.TODO()- placeholder root context, signals 'context plumbing incomplete'
- context.WithCancel(parent)- returns ctx + cancel func to cancel manually
- context.WithTimeout(parent, d)- cancels automatically after duration d
- context.WithDeadline(parent, t)- cancels automatically at absolute time t
- context.WithValue(parent, k, v)- attaches a request-scoped key/value pair
- ctx.Done()- channel closed when the context is canceled or times out
- ctx.Err()- non-nil reason after Done() closes: Canceled or DeadlineExceeded
Fan-Out with errgroup.WithContext
errgroup.WithContext cancels the shared context the moment any goroutine returns a non-nil error, stopping the siblings early.
import "golang.org/x/sync/errgroup"func fetchAll(ctx context.Context, urls []string) ([]string, error) { g, ctx := errgroup.WithContext(ctx) results := make([]string, len(urls)) for i, url := range urls { i, url := i, url // capture per-iteration (pre-Go 1.22 idiom, still safe on newer) g.Go(func() error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err // first error cancels ctx for all other in-flight requests } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) results[i] = string(body) return err }) } if err := g.Wait(); err != nil { return nil, err } return results, nil}
context.WithCancelCause & context.Cause
Since Go 1.20, WithCancelCause lets you attach a specific reason to a cancellation instead of the generic context.Canceled.
ctx, cancel := context.WithCancelCause(context.Background())go func() { time.Sleep(50 * time.Millisecond) cancel(fmt.Errorf("upstream circuit breaker tripped"))}()<-ctx.Done()fmt.Println(ctx.Err()) // context.Canceled (generic, for compatibility)fmt.Println(context.Cause(ctx)) // "upstream circuit breaker tripped" (the real reason)// WithTimeoutCause / WithDeadlineCause (Go 1.21+) attach a cause to timeouts tooctx2, cancel2 := context.WithTimeoutCause(context.Background(), 2*time.Second, fmt.Errorf("db query budget exceeded"))defer cancel2()
WithoutCancel & AfterFunc (Go 1.21+)
WithoutCancel detaches a value-carrying context from its parent's cancellation; AfterFunc schedules cleanup without a dedicated goroutine.
func handleAsync(ctx context.Context) { // Background work that must survive the request context being canceled // (e.g. audit logging) but should still see request-scoped values. detached := context.WithoutCancel(ctx) go func() { auditLog(detached, "request completed") }()}func withCleanup(ctx context.Context, conn *sql.Conn) { // AfterFunc runs fn in its own goroutine once ctx is Done, without // requiring a select{}/for-loop goroutine just to wait on Done(). stop := context.AfterFunc(ctx, func() { conn.Close() }) defer stop() // stop() cancels the registration if cleanup already ran or is unneeded}
Implementing context.Context Yourself
The interface has only four methods — useful when wrapping a context with cheap, non-cancelable metadata without importing WithValue's linear lookup chain.
// A minimal read-only wrapper that overrides only Value(), delegating// everything else — this is essentially what context.WithValue returns.type loggerContext struct { context.Context logger *slog.Logger}func (c *loggerContext) Value(key any) any { if key == loggerKey{} { return c.logger } return c.Context.Value(key) // fall through to parent}func WithLogger(ctx context.Context, l *slog.Logger) context.Context { return &loggerContext{Context: ctx, logger: l}}// Deadline(), Done(), and Err() are inherited via the embedded Context,// so cancellation still propagates correctly from the parent.
Context Anti-Patterns & Gotchas
Mistakes that pass code review but cause leaks or subtle bugs in production.
- Storing ctx in a struct field- go vet and the Go team explicitly discourage this; pass ctx as the first argument to every call chain instead
- Passing nil instead of context.TODO()- a nil Context panics on ctx.Done()/Value() calls; always use TODO() as the explicit placeholder
- Using WithValue for optional parameters- makes function signatures lie about their dependencies; reserve it for cross-cutting request-scoped metadata (trace IDs, auth principal)
- Forgetting defer cancel() in a loop- each WithTimeout/WithCancel call in a loop leaks a timer until the parent context ends if cancel isn't called per-iteration
- Re-deriving from context.Background() mid-chain- silently drops the caller's deadline/cancellation and values; always derive from the ctx you were given
- Checking ctx.Err() instead of selecting on ctx.Done()- a polling loop that only checks Err() without also selecting on Done() can busy-spin instead of blocking efficiently
Always call the cancel function returned by WithCancel/WithTimeout/WithDeadline, typically via defer cancel(), even when the operation succeeds — skipping it leaks the internal timer/goroutine until the parent context itself is canceled.