1. go
  2. /concurrency
  3. /context

Understanding Context in Go Programming

The context package provides a way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between processes. This guide covers everything you need to know about using context effectively.

Context Basics

Creating Contexts

// Background context
ctx := context.Background()

// TODO context (for when you're not sure what context to use)
ctx := context.TODO()

// Context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Context with deadline
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

Context Values

// Adding values to context
ctx := context.WithValue(context.Background(), "key", "value")

// Retrieving values
value := ctx.Value("key").(string)

// Type-safe context keys
type contextKey string
const userKey contextKey = "user"

ctx = context.WithValue(ctx, userKey, User{Name: "Alice"})
user := ctx.Value(userKey).(User)

Best Practices

1. Context Propagation

// Good: Pass context as first parameter
func ProcessRequest(ctx context.Context, req *Request) error {
    // Check if context is done
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        // Process request
    }
    return nil
}

// Bad: Context buried in struct
type BadService struct {
    ctx context.Context
}

// Good: Context passed explicitly
type GoodService struct {
    // No context here
}

func (s *GoodService) Process(ctx context.Context) error {
    // Use context here
    return nil
}

2. Cancellation Handling

func worker(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do work
            if err := doWork(); err != nil {
                return err
            }
        }
    }
}

// Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

go worker(ctx)

3. Value Types

// Good: Use custom types for context keys
type key int

const (
    requestIDKey key = iota
    userIDKey
)

// Good: Wrap context values
type RequestScope struct {
    RequestID string
    UserID    string
}

func WithRequestScope(ctx context.Context, rs RequestScope) context.Context {
    return context.WithValue(ctx, requestIDKey, rs)
}

func GetRequestScope(ctx context.Context) (RequestScope, bool) {
    rs, ok := ctx.Value(requestIDKey).(RequestScope)
    return rs, ok
}

Common Patterns

1. HTTP Server

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // Add request-scoped values
    ctx = context.WithValue(ctx, "requestID", uuid.New().String())
    
    // Create child context with timeout
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    
    select {
    case <-ctx.Done():
        http.Error(w, "Request timed out", http.StatusGatewayTimeout)
        return
    case result := <-processRequest(ctx):
        fmt.Fprintf(w, "Result: %v", result)
    }
}

func processRequest(ctx context.Context) <-chan string {
    ch := make(chan string)
    go func() {
        defer close(ch)
        // Simulate work
        select {
        case <-ctx.Done():
            return
        case <-time.After(time.Second):
            ch <- "processed"
        }
    }()
    return ch
}

2. Database Operations

type DB struct {
    pool *sql.DB
}

func (db *DB) QueryWithTimeout(ctx context.Context, query string) (*sql.Rows, error) {
    // Add timeout for database operations
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    // Execute query with context
    rows, err := db.pool.QueryContext(ctx, query)
    if err != nil {
        return nil, fmt.Errorf("query failed: %w", err)
    }
    return rows, nil
}

3. Worker Pool

type WorkPool struct {
    workers int
    jobs    chan Job
}

func (p *WorkPool) Start(ctx context.Context) {
    for i := 0; i < p.workers; i++ {
        go func() {
            for {
                select {
                case job := <-p.jobs:
                    select {
                    case <-ctx.Done():
                        return
                    default:
                        job.Execute(ctx)
                    }
                case <-ctx.Done():
                    return
                }
            }
        }()
    }
}

Performance Considerations

1. Context Chain Length

// Bad: Long context chain
ctx1 := context.WithValue(context.Background(), "key1", "value1")
ctx2 := context.WithValue(ctx1, "key2", "value2")
ctx3 := context.WithValue(ctx2, "key3", "value3")
// ... and so on

// Better: Group related values
type RequestContext struct {
    Key1, Key2, Key3 string
}
ctx := context.WithValue(context.Background(), "request", RequestContext{
    Key1: "value1",
    Key2: "value2",
    Key3: "value3",
})

2. Value Lookup Cost

// Expensive: Frequent context value lookups
func processRequest(ctx context.Context) {
    for i := 0; i < 1000; i++ {
        id := ctx.Value("requestID").(string)
        // Use id
    }
}

// Better: Cache frequently used values
func processRequest(ctx context.Context) {
    id := ctx.Value("requestID").(string)
    for i := 0; i < 1000; i++ {
        // Use id
    }
}

Common Mistakes

1. Storing Request Objects

// Wrong: Storing request object in context
type Request struct {
    Method string
    Path   string
}

ctx := context.WithValue(context.Background(), "request", &Request{
    Method: "GET",
    Path:   "/api",
})

// Right: Store only request-scoped values
ctx := context.WithValue(context.Background(), "requestID", "123")
ctx = context.WithValue(ctx, "userID", "456")

2. Context in Structs

// Wrong: Storing context in struct
type Service struct {
    ctx context.Context
    // other fields
}

// Right: Pass context to methods
type Service struct {
    // other fields
}

func (s *Service) Process(ctx context.Context) error {
    // Use context here
    return nil
}

3. Cancellation Propagation

// Wrong: Not propagating cancellation
func (s *Service) Process(ctx context.Context) error {
    // Create new context without parent
    newCtx := context.Background()
    return s.worker.Do(newCtx)
}

// Right: Propagate parent context
func (s *Service) Process(ctx context.Context) error {
    // Create child context
    childCtx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    return s.worker.Do(childCtx)
}

Next Steps

  1. Learn about goroutines
  2. Explore channels
  3. Study select
  4. Practice with timeouts

Additional Resources