Context
What is Context?
The context package provides a way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines. It’s the standard mechanism for controlling the lifecycle of operations in Go.
Every context has a parent, forming a tree. When a parent context is cancelled, all its children are cancelled too. This makes it possible to cancel an entire tree of operations with a single call.
graph TD
A[Background Context] --> B[Request Context]
B --> C[Database Query]
B --> D[HTTP Call]
B --> E[Cache Lookup]
A:::root
B:::request
C:::operation
D:::operation
E:::operation
classDef root fill:#748796,stroke:#333,stroke-width:2px
classDef request fill:#118098,stroke:#333,stroke-width:2px
classDef operation fill:#077fed,stroke:#333,stroke-width:2px
When the request context is cancelled, all three operations (database query, HTTP call, cache lookup) are cancelled simultaneously.
Creating Contexts
Background and TODO
context.Background() returns an empty context. It’s the root of any context tree and is typically used in main(), initialization, and tests.
ctx := context.Background()context.TODO() is also an empty context, but signals that you haven’t decided which context to use yet. Use it as a placeholder when refactoring code to add context support.
ctx := context.TODO() // Replace with proper context later.WithCancel
context.WithCancel returns a copy of the parent context with a new Done channel. The returned cancel function must be called to release resources.
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Always defer cancel to avoid leaks.
go worker(ctx)
time.Sleep(2 * time.Second)
cancel() // Signal the worker to stop.
time.Sleep(100 * time.Millisecond)
}
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker stopped:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}WithCancelCause (Go 1.20+)
context.WithCancelCause lets you provide a reason when cancelling, which can be retrieved later with context.Cause.
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel(errors.New("shutting down for maintenance"))
}
func worker(ctx context.Context) {
<-ctx.Done()
fmt.Println("Stopped:", ctx.Err()) // context canceled
fmt.Println("Cause:", context.Cause(ctx)) // shutting down for maintenance
}This is useful for debugging and loggingβyou can distinguish between different reasons for cancellation.
WithTimeout and WithDeadline
context.WithTimeout creates a context that automatically cancels after a duration. context.WithDeadline does the same but with an absolute time.
func fetchData(ctx context.Context) error {
// Create a context that times out after 3 seconds.
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("fetch data: request timed out: %w", err)
}
return fmt.Errorf("fetch data: %w", err)
}
defer resp.Body.Close()
return nil
}The difference between WithTimeout and WithDeadline:
// These are equivalent.
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Second))Use WithTimeout when you think in terms of duration (“wait at most 5 seconds”). Use WithDeadline when you have a fixed point in time (“must complete by 2pm”).
WithoutCancel (Go 1.21+)
context.WithoutCancel returns a copy of the parent that is not cancelled when the parent is cancelled. This is useful when you need to perform cleanup operations that should complete even after the main request is cancelled.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Main work that should respect cancellation.
result, err := doWork(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Audit logging should complete even if client disconnects.
go func() {
auditCtx := context.WithoutCancel(ctx)
auditCtx, cancel := context.WithTimeout(auditCtx, 5*time.Second)
defer cancel()
logAuditEvent(auditCtx, result)
}()
json.NewEncoder(w).Encode(result)
}AfterFunc (Go 1.21+)
context.AfterFunc registers a function to run after a context is cancelled. The function runs in its own goroutine.
func processWithCleanup(ctx context.Context, resource *Resource) error {
// Register cleanup to run when context is cancelled.
stop := context.AfterFunc(ctx, func() {
resource.Cleanup()
})
defer stop() // Cancel the AfterFunc if we return normally.
return resource.Process(ctx)
}Listening for Cancellation
The Done() method returns a channel that closes when the context is cancelled. Use it in a select statement to respond to cancellation.
func longOperation(ctx context.Context) error {
resultCh := make(chan string)
go func() {
// Simulate work.
time.Sleep(5 * time.Second)
resultCh <- "completed"
}()
select {
case result := <-resultCh:
fmt.Println("Operation", result)
return nil
case <-ctx.Done():
return fmt.Errorf("long operation cancelled: %w", ctx.Err())
}
}Checking Context Errors
When a context is cancelled, ctx.Err() returns one of two sentinel errors:
context.Canceledβ the context was explicitly cancelled viacancel()context.DeadlineExceededβ the context’s deadline passed
Use errors.Is() to check for these errors:
if err := ctx.Err(); err != nil {
if errors.Is(err, context.Canceled) {
log.Println("Operation was cancelled")
} else if errors.Is(err, context.DeadlineExceeded) {
log.Println("Operation timed out")
}
}To get the underlying cause (if WithCancelCause was used):
if cause := context.Cause(ctx); cause != nil {
log.Printf("Cancellation cause: %v", cause)
}Passing Values
context.WithValue attaches a key-value pair to a context. This is useful for request-scoped data like request IDs, authentication tokens, or tracing information.
type contextKey string
const requestIDKey contextKey = "requestID"
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String()
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
requestID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
requestID = "unknown"
}
log.Printf("[%s] Handling request", requestID)
}Value Best Practices
Use custom types for keys. This prevents collisions between packages that might use the same string key.
// Good: custom type prevents collisions.
type contextKey string
const userKey contextKey = "user"
// Bad: string keys can collide.
ctx = context.WithValue(ctx, "user", user) // Another package might use "user" too.Don’t use context for optional parameters. Context values are for request-scoped data that transits process boundaries, not for passing function arguments.
// Bad: using context to pass configuration.
ctx = context.WithValue(ctx, "retryCount", 3)
doSomething(ctx)
// Good: use explicit parameters.
doSomething(ctx, RetryConfig{Count: 3})Keep values immutable. Don’t store pointers to mutable data that you’ll modify later. Context values should be read-only.
Context Propagation Patterns
HTTP Handlers
HTTP requests come with a context attached. Use r.Context() and propagate it to downstream calls.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := fetchUser(ctx, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
orders, err := fetchOrders(ctx, user.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Continue processing...
}Database Operations
Most database drivers accept context for query cancellation.
func getUser(ctx context.Context, db *sql.DB, id int) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return &user, nil
}Goroutines
When spawning goroutines, pass the context explicitly.
func processItems(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
g.Go(func() error {
return processItem(ctx, item)
})
}
return g.Wait()
}Note: In Go versions before 1.22, you needed to capture the loop variable with
item := itembefore the goroutine. As of Go 1.22, loop variables are created fresh for each iteration, so this is no longer necessary.
Common Anti-Patterns
Storing context in structs
Context should flow through your program as a function parameter, not be stored in structs.
// Bad: context stored in struct.
type Service struct {
ctx context.Context
db *sql.DB
}
// Good: context passed per-call.
type Service struct {
db *sql.DB
}
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
return getUser(ctx, s.db, id)
}Passing nil context
Never pass a nil context. Use context.TODO() if you’re unsure, or context.Background() for top-level calls.
// Bad: nil context can cause panics.
doSomething(nil)
// Good: explicit empty context.
doSomething(context.Background())Ignoring cancellation
If you accept a context, respect it. Check ctx.Done() in long-running operations.
// Bad: ignores context.
func process(ctx context.Context, items []Item) {
for _, item := range items {
processItem(item) // What if context is cancelled?
}
}
// Good: checks context.
func process(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done():
return fmt.Errorf("process items: %w", ctx.Err())
default:
processItem(item)
}
}
return nil
}Summary
| Function | Purpose | Added |
|---|---|---|
Background() |
Root context for main, init, tests | Go 1.7 |
TODO() |
Placeholder when context is unclear | Go 1.7 |
WithCancel() |
Manual cancellation | Go 1.7 |
WithCancelCause() |
Cancellation with reason | Go 1.20 |
WithTimeout() |
Cancel after duration | Go 1.7 |
WithDeadline() |
Cancel at specific time | Go 1.7 |
WithValue() |
Attach request-scoped data | Go 1.7 |
WithoutCancel() |
Detach from parent cancellation | Go 1.21 |
AfterFunc() |
Run function on cancellation | Go 1.21 |
Cause() |
Get cancellation reason | Go 1.20 |