Concurrency Created: 2024-01-27 Updated: 2024-01-27

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