Context Propagation
A pattern for passing request-scoped values, cancellation signals, and deadlines through the call chain.
Contributors
When to Use
- When operations need cancellation or timeout support
- When request-scoped values must flow through the call stack
- For any I/O operation that should respect caller timeouts
When NOT to Use
- For passing optional function parameters (use functional options instead)
- For dependency injection of services
- When the value isn't truly request-scoped
Context propagation is a fundamental Go pattern for managing request lifecycles. The context.Context type carries deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines.
Implementation
package service
import (
"context"
"fmt"
"time"
)
type UserService struct {
repo *UserRepository
}
// Always accept context as the first parameter
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
// Check if context is already cancelled
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("context cancelled: %w", err)
}
// Pass context to downstream calls
return s.repo.FindByID(ctx, id)
}
func (s *UserService) ProcessUsers(ctx context.Context, ids []int) error {
for _, id := range ids {
// Check context before each iteration
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if _, err := s.GetUser(ctx, id); err != nil {
return err
}
}
return nil
}
// Context with request-scoped values
type requestIDKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func RequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey{}).(string); ok {
return id
}
return ""
}
Usage
package main
import (
"context"
"log"
"time"
)
func main() {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Always call cancel to release resources
// Add request-scoped value
ctx = WithRequestID(ctx, "req-123")
svc := &UserService{repo: NewUserRepository()}
user, err := svc.GetUser(ctx, 42)
if err != nil {
log.Printf("[%s] error: %v", RequestID(ctx), err)
return
}
log.Printf("[%s] found user: %s", RequestID(ctx), user.Name)
}
// HTTP handler example
func handleRequest(w http.ResponseWriter, r *http.Request) {
// http.Request already has a context
ctx := r.Context()
// Add timeout for this specific operation
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
result, err := expensiveOperation(ctx)
// ...
}
Benefits
- Cancellation propagation: All operations in the chain stop when the context is cancelled
- Deadline enforcement: Operations automatically timeout based on context deadline
- Request tracing: Request IDs and trace information flow naturally through the stack
- Resource cleanup: Goroutines can detect cancellation and clean up
- Standard interface: Works consistently across the standard library and third-party packages