Context Patterns
Context usage for cancellation, timeouts, deadlines, and value propagation in Go
You are an expert in Go's `context` package, helping developers correctly use contexts for cancellation, deadlines, timeouts, and request-scoped value propagation. ## Key Points - `context.Background()` — top-level context for main, init, and tests. - `context.TODO()` — placeholder when unsure which context to use. - `context.WithCancel(parent)` — returns a context cancelled when `cancel()` is called. - `context.WithTimeout(parent, duration)` — cancelled after the duration. - `context.WithDeadline(parent, time)` — cancelled at a specific time. - `context.WithValue(parent, key, val)` — carries a key-value pair. - `context.WithoutCancel(parent)` — (Go 1.21+) derives a context that is not cancelled when the parent is. - Pass `context.Context` as the first parameter of every function that does I/O or may block: `func Foo(ctx context.Context, ...) error`. - Always call the `cancel` function returned by `WithCancel`/`WithTimeout`/`WithDeadline`, typically with `defer cancel()`. - Use `context.WithValue` only for request-scoped data that transits process boundaries (request IDs, auth tokens), not for passing function parameters. - Use typed, unexported keys for context values to avoid collisions. - Prefer `signal.NotifyContext` for OS signal handling instead of manual channel setup.
skilldb get go-skills/Context PatternsFull skill: 257 linesContext Patterns — Go Programming
You are an expert in Go's context package, helping developers correctly use contexts for cancellation, deadlines, timeouts, and request-scoped value propagation.
Core Philosophy
Context is Go's answer to the question of how to manage the lifecycle of operations that span multiple goroutines, network calls, and service boundaries. Rather than relying on global timeouts or ad-hoc cancellation flags, the context package gives every operation an explicit scope — a parent-child tree where cancellation flows downward and deadlines are inherited. This design makes it possible to tear down an entire request's worth of concurrent work with a single cancel() call, without any goroutine needing to know about the others.
The key insight behind context is that every piece of work in a server should be tied to the request that initiated it. When a client disconnects or a deadline passes, all downstream operations — database queries, RPC calls, file reads — should stop promptly. Context makes this cooperative: functions check ctx.Done() or pass the context to libraries that do. This is not automatic garbage collection of goroutines; it is an explicit contract that every layer of your code opts into by accepting and forwarding a context.Context.
Context values are the most misunderstood part of the package. They exist for request-scoped metadata that crosses API boundaries — trace IDs, authentication tokens, request identifiers — not for passing function arguments or configuration. When you find yourself reaching for context.WithValue, ask whether the data is truly request-scoped and whether it needs to transit process boundaries. If not, pass it as a regular function parameter instead.
Anti-Patterns
-
Using context for dependency injection: Stuffing database connections, loggers, or service clients into context values turns your function signatures into lies — the real dependencies are hidden inside an opaque
context.Contextinstead of being visible in the parameter list. Pass dependencies explicitly through constructors or function parameters. -
Creating a fresh
context.Background()deep in the call stack: This severs the cancellation chain from the original request context. When the parent request is cancelled, your downstream operation keeps running because it has no idea the parent is gone. Always derive from the context passed to you. -
Ignoring context in CPU-bound loops: A function that accepts a context but never checks
ctx.Done()during a long computation defeats the purpose of context-based cancellation. Periodically check the context in tight loops, even if it means adding a check every N iterations. -
Storing mutable state in context values: Context values should be immutable, read-only data. Storing a pointer to a mutable struct and modifying it from multiple goroutines introduces race conditions that the context package was never designed to protect against.
-
Letting
context.TODO()persist into production code:TODOis a development placeholder signaling "I haven't decided which context to use yet." Shipping it means you skipped the decision. EveryTODOshould be replaced with a real context derived from the caller before the code is merged.
Overview
The context package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines. Almost every Go function that performs I/O or long-running work should accept a context.Context as its first parameter.
Core Concepts
Context Tree
Contexts form a tree rooted at context.Background() (or context.TODO()). When a parent context is cancelled, all derived child contexts are also cancelled.
Context Types
context.Background()— top-level context for main, init, and tests.context.TODO()— placeholder when unsure which context to use.context.WithCancel(parent)— returns a context cancelled whencancel()is called.context.WithTimeout(parent, duration)— cancelled after the duration.context.WithDeadline(parent, time)— cancelled at a specific time.context.WithValue(parent, key, val)— carries a key-value pair.context.WithoutCancel(parent)— (Go 1.21+) derives a context that is not cancelled when the parent is.
ctx.Done()
Returns a channel that is closed when the context is cancelled or times out. Use in select to stop work.
ctx.Err()
Returns context.Canceled or context.DeadlineExceeded after the context is done.
Implementation Patterns
Cancellation
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go worker(ctx)
time.Sleep(3 * time.Second)
cancel() // signal worker to stop
time.Sleep(100 * time.Millisecond) // let worker clean up
}
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
slog.Info("worker stopping", "reason", ctx.Err())
return
default:
doWork()
}
}
}
Timeout
func fetchData(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Deadline
func processJob(ctx context.Context, job Job) error {
deadline := time.Now().Add(30 * time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
// Check remaining time
if dl, ok := ctx.Deadline(); ok {
remaining := time.Until(dl)
slog.Info("time budget", "remaining", remaining)
}
return job.Execute(ctx)
}
Context Values (Request-Scoped Data)
// Use unexported key types to prevent collisions
type contextKey string
const (
requestIDKey contextKey = "requestID"
userKey contextKey = "user"
)
// Setter
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
// Getter
func RequestIDFrom(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey).(string)
return id, ok
}
// Middleware
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
ctx := WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Database Operations with Context
func (r *UserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
query := `SELECT id, name, email FROM users WHERE id = $1`
var u User
err := r.db.QueryRowContext(ctx, query, id).Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("querying user %d: %w", id, err)
}
return &u, nil
}
Graceful Shutdown with Context
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "err", err)
}
}()
<-ctx.Done()
slog.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown error", "err", err)
}
}
Context-Aware Long-Running Loop
func poll(ctx context.Context, interval time.Duration, fn func(context.Context) error) error {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
if err := fn(ctx); err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
AfterFunc (Go 1.21+)
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
stop := context.AfterFunc(ctx, func() {
// Runs in a new goroutine when ctx is cancelled
conn.Close()
})
// Call stop() if you want to prevent the callback from firing
defer stop()
Best Practices
- Pass
context.Contextas the first parameter of every function that does I/O or may block:func Foo(ctx context.Context, ...) error. - Always call the
cancelfunction returned byWithCancel/WithTimeout/WithDeadline, typically withdefer cancel(). - Use
context.WithValueonly for request-scoped data that transits process boundaries (request IDs, auth tokens), not for passing function parameters. - Use typed, unexported keys for context values to avoid collisions.
- Prefer
signal.NotifyContextfor OS signal handling instead of manual channel setup. - Never store contexts in structs. Pass them explicitly through function calls.
- Use
context.WithoutCancel(Go 1.21+) when a child operation must outlive the parent (e.g., flushing logs on shutdown).
Common Pitfalls
- Not checking
ctx.Err()in loops: a long computation loop should periodically check if the context is cancelled. - Forgetting
defer cancel(): leaks resources (timers, goroutines) held by the context. - Using
context.Background()everywhere: bypasses cancellation propagation. Accept a context from the caller. - Using
context.TODO()in production: it is a development marker; replace it with a real context before shipping. - Storing large objects in context values: context values are copied on derivation; keep them small (strings, small structs).
- Expecting context values to be type-safe:
ctx.Valuereturnsany; always type-assert and handle the missing-value case. - Creating a new
context.Background()in middleware: this breaks the parent chain. Derive fromr.Context()instead.
Install this skill directly: skilldb add go-skills
Related Skills
Error Handling
Error handling patterns including wrapping, sentinel errors, custom types, and error groups in Go
Generics
Go generics including type parameters, constraints, and generic data structure patterns (Go 1.18+)
Goroutines Channels
Concurrency patterns using goroutines, channels, select, and sync primitives in Go
HTTP Servers
HTTP server patterns using net/http, chi, and gin including middleware, routing, and graceful shutdown
Interfaces
Interface design principles, implicit satisfaction, and composition patterns in Go
Modules
Go modules, dependency management, versioning, workspaces, and private module configuration