Root Contexts
Background ยท TODOEvery context tree starts from one of two root values. You never create a context from scratch โ you always derive from a root or from another context.
context.Background
Background
// Background โ the top-level root context // Never cancelled, has no deadline, no values // Use as the starting point in main, tests, and // at the top of incoming request handlers ctx := context.Background() // Typical patterns func main() { ctx := context.Background() run(ctx) } func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // already set by net/http doWork(ctx) }
context.TODO
TODO
// TODO โ a placeholder when you're not yet sure // which context to use, or the calling function // doesn't accept one yet ctx := context.TODO() // Use TODO to signal "this needs to be wired up" // It behaves identically to Background at runtime // but communicates intent to future readers // Tools like staticcheck can detect TODO contexts // and remind you to replace them // Rule of thumb: // - Background: you know this is the right root // - TODO: you know this is temporary
WithCancel
Manual cancellationAlways call the
cancel function โ even if work completes successfully before cancellation. Failing to call it leaks resources until the parent context is cancelled.
WithCancel โ cancel on demand
WithCancel
ctx, cancel := context.WithCancel(context.Background()) defer cancel() // always call cancel to free resources go func() { for { select { case <-ctx.Done(): fmt.Println("worker stopped:", ctx.Err()) // ctx.Err() == context.Canceled return default: doWork() } } }() // Cancel all derived goroutines when done time.Sleep(2 * time.Second) cancel() // signals ctx.Done() to close
Cancellation propagates to all children
Propagation
parent, cancelParent := context.WithCancel(context.Background()) defer cancelParent() child1, cancel1 := context.WithCancel(parent) defer cancel1() child2, cancel2 := context.WithCancel(parent) defer cancel2() // Cancelling the parent cancels child1 and child2 too cancelParent() // child1.Err() == context.Canceled // child2.Err() == context.Canceled // Cancelling a child does NOT affect the parent or siblings cancel1() // parent.Err() == nil (still alive) // child2.Err() == nil (still alive)
WithTimeout & WithDeadline
Time-based cancellationWithTimeout(ctx, d) is shorthand for WithDeadline(ctx, time.Now().Add(d)). Use WithTimeout when you think in durations ("5 seconds"), WithDeadline when you think in absolute times ("until 3pm").
WithTimeout
WithTimeout
ctx, cancel := context.WithTimeout( context.Background(), 5*time.Second, ) defer cancel() // still call cancel to release early resp, err := http.Get("https://example.com") // if request takes > 5s, ctx is cancelled // and err wraps context.DeadlineExceeded // Check for timeout specifically if errors.Is(err, context.DeadlineExceeded) { log.Println("request timed out") }
WithDeadline
WithDeadline
// WithDeadline โ cancel at an absolute time deadline := time.Now().Add(10 * time.Second) ctx, cancel := context.WithDeadline( context.Background(), deadline, ) defer cancel() // Check when the context will expire dl, ok := ctx.Deadline() if ok { fmt.Println("expires at:", dl) fmt.Println("time left:", time.Until(dl)) }
Timeout with database / HTTP โ real-world pattern
Pattern
func fetchUser(ctx context.Context, id int) (*User, error) { // Apply a local timeout on top of whatever deadline the caller set ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() var user User err := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id, ).Scan(&user.ID, &user.Name) if err != nil { return nil, fmt.Errorf("fetchUser: %w", err) } return &user, nil } // Whichever deadline is sooner โ the caller's or the local 3s โ wins
Context Methods
Done ยท Err ยท Deadline ยท Value
The four methods on context.Context
Interface
type Context interface { // Done returns a channel that closes when the context is cancelled // or times out. Returns nil for Background and TODO (never closes). Done() <-chan struct{} // Err returns nil while the context is active. // After Done closes: context.Canceled or context.DeadlineExceeded Err() error // Deadline returns the expiry time if one is set. Deadline() (deadline time.Time, ok bool) // Value returns the value for a key set via WithValue, or nil. Value(key any) any }
Listening on Done in a select
Done
func processItems(ctx context.Context, items <-chan Item) error { for { select { case <-ctx.Done(): return ctx.Err() // Canceled or DeadlineExceeded case item, ok := <-items: if !ok { return nil // channel closed normally } if err := process(item); err != nil { return err } } } } // Check context before expensive operations func doWork(ctx context.Context) error { if err := ctx.Err(); err != nil { return err // bail early, context already done } // proceed with work... return nil }
context.Canceled vs DeadlineExceeded
Errors
// Two sentinel errors returned by Err() context.Canceled // cancel() was called context.DeadlineExceeded // timeout/deadline passed // Distinguish with errors.Is switch ctx.Err() { case context.Canceled: log.Println("cancelled by caller") case context.DeadlineExceeded: log.Println("timed out") }
Checking remaining time
Deadline
dl, ok := ctx.Deadline() if !ok { // no deadline set โ run as long as needed } remaining := time.Until(dl) if remaining < 100*time.Millisecond { return fmt.Errorf("not enough time to proceed") } // Pass a tighter deadline to a sub-call subCtx, cancel := context.WithDeadline(ctx, dl.Add(-50*time.Millisecond), // 50ms buffer ) defer cancel()
WithValue
Request-scoped dataUse
WithValue only for request-scoped data that crosses API boundaries โ things like trace IDs, auth tokens, or locale. Don't use it as a shortcut for passing optional function parameters.
WithValue โ typed keys prevent collisions
WithValue
// Always use an unexported custom type as the key // so no other package can accidentally read or overwrite it type contextKey string const ( keyTraceID contextKey = "traceID" keyUserID contextKey = "userID" ) // Store a value ctx := context.WithValue(r.Context(), keyTraceID, "abc-123") ctx = context.WithValue(ctx, keyUserID, 42) // Retrieve a value โ returns any, so assert the type traceID, ok := ctx.Value(keyTraceID).(string) if !ok { traceID = "unknown" } userID, _ := ctx.Value(keyUserID).(int)
Package-level helper functions โ the idiomatic pattern
Pattern
// Define the key type privately in your package type traceKey struct{} // Expose typed helper functions instead of the raw key func WithTraceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, traceKey{}, id) } func TraceID(ctx context.Context) (string, bool) { id, ok := ctx.Value(traceKey{}).(string) return id, ok } // Usage โ callers never touch the key directly ctx = WithTraceID(ctx, "req-789") if id, ok := TraceID(ctx); ok { log.Printf("trace=%s", id) }
Common Patterns
HTTP ยท goroutines ยท signals
HTTP server โ context flows from request to dependencies
HTTP
func handler(w http.ResponseWriter, r *http.Request) { // r.Context() is cancelled when the client disconnects ctx := r.Context() // Add a per-request timeout ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() // Attach trace ID from incoming header ctx = WithTraceID(ctx, r.Header.Get("X-Trace-ID")) // Pass ctx to every downstream call user, err := fetchUser(ctx, 123) if err != nil { if errors.Is(err, context.DeadlineExceeded) { http.Error(w, "timeout", http.StatusGatewayTimeout) return } http.Error(w, "internal error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) }
Graceful shutdown with os.Signal
Shutdown
func main() { ctx, stop := signal.NotifyContext( context.Background(), syscall.SIGINT, syscall.SIGTERM, ) defer stop() server := &http.Server{Addr: ":8080"} go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }() // Block until SIGINT or SIGTERM <-ctx.Done() log.Println("shutting down...") shutdownCtx, cancel := context.WithTimeout( context.Background(), 10*time.Second, ) defer cancel() server.Shutdown(shutdownCtx) }
Fan-out โ cancel all workers when the first fails
Fan-out
func fetchAll(ctx context.Context, urls []string) ([]string, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() results := make([]string, len(urls)) errCh := make(chan error, len(urls)) var wg sync.WaitGroup for i, url := range urls { wg.Add(1) go func(i int, url string) { defer wg.Done() body, err := fetch(ctx, url) if err != nil { cancel() // cancel all siblings errCh <- err return } results[i] = body }(i, url) } wg.Wait() close(errCh) return results, <-errCh }
Quick Reference
All key functions & values| Function / Value | Returns | Description |
|---|---|---|
| context.Background() | Context | Root context; never cancelled, no deadline, no values |
| context.TODO() | Context | Placeholder root; signals "this should be wired up later" |
| context.WithCancel(parent) | Context, CancelFunc | New context cancelled when parent is or cancel() is called |
| context.WithTimeout(parent, d) | Context, CancelFunc | Cancelled after duration d or when parent is cancelled |
| context.WithDeadline(parent, t) | Context, CancelFunc | Cancelled at absolute time t or when parent is cancelled |
| context.WithValue(parent, k, v) | Context | New context carrying value v under key k |
| ctx.Done() | <-chan struct{} | Closed when context is cancelled or times out; nil for Background/TODO |
| ctx.Err() | error | nil while active; Canceled or DeadlineExceeded after Done closes |
| ctx.Deadline() | time.Time, bool | Returns the deadline and whether one is set |
| ctx.Value(key) | any | Returns value stored under key, or nil |
| context.Canceled | error | Sentinel: cancel() was called |
| context.DeadlineExceeded | error | Sentinel: deadline or timeout passed |
| signal.NotifyContext(ctx, sigs...) | Context, StopFunc | Cancel context on OS signals (os/signal package) |