Skip to main content

The Context Package

The context package is essential for production Go. It provides a standardized way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines.

Why Context?

In real-world applications, you need to:
  • Cancel operations when a client disconnects
  • Set timeouts for database queries, HTTP requests
  • Pass request-scoped data like request IDs, user info
  • Propagate cancellation through a chain of function calls
Without context, you’d need to implement these features manually for every function.

Context Basics

The Context Interface

type Context interface {
    // Deadline returns the time when work should be cancelled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that's closed when work should be cancelled
    Done() <-chan struct{}
    
    // Err returns why the context was cancelled
    Err() error
    
    // Value returns the value associated with a key
    Value(key interface{}) interface{}
}

Creating Contexts

// Background context - the root of any context tree
ctx := context.Background()

// TODO context - use when unsure which context to use (placeholder)
ctx := context.TODO()

Cancellation

context.WithCancel

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    go worker(ctx)
    
    time.Sleep(3 * time.Second)
    cancel() // Signal worker to stop
    
    time.Sleep(1 * time.Second) // Give worker time to clean up
}

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker: Received cancellation signal")
            fmt.Println("Worker: Reason:", ctx.Err())
            return
        default:
            fmt.Println("Worker: Working...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

Cancellation Propagation

Child contexts are cancelled when their parent is cancelled:
func main() {
    parent, parentCancel := context.WithCancel(context.Background())
    
    child1, _ := context.WithCancel(parent)
    child2, _ := context.WithCancel(parent)
    
    go func() {
        <-child1.Done()
        fmt.Println("Child 1 cancelled")
    }()
    
    go func() {
        <-child2.Done()
        fmt.Println("Child 2 cancelled")
    }()
    
    parentCancel() // Cancels parent AND both children
    time.Sleep(100 * time.Millisecond)
}

Timeouts and Deadlines

context.WithTimeout

func fetchWithTimeout(url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // Always call cancel to release resources
    
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("request timed out after 5 seconds")
        }
        return nil, err
    }
    defer resp.Body.Close()
    
    return io.ReadAll(resp.Body)
}

context.WithDeadline

func processOrder(orderID string) error {
    // Must complete by midnight
    midnight := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour)
    ctx, cancel := context.WithDeadline(context.Background(), midnight)
    defer cancel()
    
    return processOrderWithContext(ctx, orderID)
}

Checking Deadline

func longOperation(ctx context.Context) error {
    deadline, hasDeadline := ctx.Deadline()
    if hasDeadline {
        remaining := time.Until(deadline)
        if remaining < time.Minute {
            return errors.New("not enough time to complete operation")
        }
        fmt.Printf("Have %v to complete operation\n", remaining)
    }
    
    // Do work...
    return nil
}

Context Values

Storing and Retrieving Values

type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, requestIDKey, "req-123")
    ctx = context.WithValue(ctx, userIDKey, "user-456")
    
    handleRequest(ctx)
}

func handleRequest(ctx context.Context) {
    requestID := ctx.Value(requestIDKey).(string)
    userID := ctx.Value(userIDKey).(string)
    
    fmt.Printf("Request %s by user %s\n", requestID, userID)
}

Type-Safe Context Values

type RequestContext struct {
    RequestID string
    UserID    string
    TraceID   string
    StartTime time.Time
}

type requestContextKey struct{}

func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context {
    return context.WithValue(ctx, requestContextKey{}, rc)
}

func GetRequestContext(ctx context.Context) (*RequestContext, bool) {
    rc, ok := ctx.Value(requestContextKey{}).(*RequestContext)
    return rc, ok
}

// Usage
func handler(w http.ResponseWriter, r *http.Request) {
    rc := &RequestContext{
        RequestID: uuid.New().String(),
        UserID:    getUserID(r),
        TraceID:   r.Header.Get("X-Trace-ID"),
        StartTime: time.Now(),
    }
    
    ctx := WithRequestContext(r.Context(), rc)
    processRequest(ctx)
}

func processRequest(ctx context.Context) {
    if rc, ok := GetRequestContext(ctx); ok {
        log.Printf("[%s] Processing request for user %s", rc.RequestID, rc.UserID)
    }
}

Context in HTTP Servers

Request Context

Every http.Request carries a context:
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // Already has deadline if client sets timeout
    
    result, err := doExpensiveOperation(ctx)
    if err != nil {
        if ctx.Err() == context.Canceled {
            // Client disconnected
            log.Println("Client disconnected")
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    json.NewEncoder(w).Encode(result)
}

func doExpensiveOperation(ctx context.Context) (*Result, error) {
    resultCh := make(chan *Result, 1)
    errCh := make(chan error, 1)
    
    go func() {
        // Long running operation
        result, err := queryDatabase()
        if err != nil {
            errCh <- err
            return
        }
        resultCh <- result
    }()
    
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    case err := <-errCh:
        return nil, err
    case result := <-resultCh:
        return result, nil
    }
}

Adding Request Timeout

func timeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), timeout)
            defer cancel()
            
            r = r.WithContext(ctx)
            
            done := make(chan struct{})
            go func() {
                next.ServeHTTP(w, r)
                close(done)
            }()
            
            select {
            case <-done:
                return
            case <-ctx.Done():
                http.Error(w, "Request timeout", http.StatusGatewayTimeout)
            }
        })
    }
}

Context in Database Operations

With sql.DB

func (r *UserRepository) GetUser(ctx context.Context, id string) (*User, error) {
    query := "SELECT id, name, email FROM users WHERE id = $1"
    
    row := r.db.QueryRowContext(ctx, query, id)
    
    var user User
    if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("database query timed out: %w", err)
        }
        return nil, err
    }
    
    return &user, nil
}

func (r *UserRepository) CreateUser(ctx context.Context, user *User) error {
    query := "INSERT INTO users (id, name, email) VALUES ($1, $2, $3)"
    
    _, err := r.db.ExecContext(ctx, query, user.ID, user.Name, user.Email)
    return err
}

Transaction with Context

func (r *OrderRepository) CreateOrder(ctx context.Context, order *Order) error {
    tx, err := r.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    
    defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    
    // Insert order
    _, err = tx.ExecContext(ctx,
        "INSERT INTO orders (id, user_id, total) VALUES ($1, $2, $3)",
        order.ID, order.UserID, order.Total,
    )
    if err != nil {
        return err
    }
    
    // Insert order items
    for _, item := range order.Items {
        _, err = tx.ExecContext(ctx,
            "INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)",
            order.ID, item.ProductID, item.Quantity,
        )
        if err != nil {
            return err
        }
    }
    
    return tx.Commit()
}

Context Best Practices

DO’s

// ✅ Pass context as the first parameter
func ProcessOrder(ctx context.Context, orderID string) error

// ✅ Use context.Background() at the top of main, tests, and init
func main() {
    ctx := context.Background()
    run(ctx)
}

// ✅ Always call cancel when done
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// ✅ Check ctx.Done() in long-running loops
for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        // do work
    }
}

// ✅ Use typed keys for context values
type requestIDKey struct{}
ctx = context.WithValue(ctx, requestIDKey{}, "123")

DON’Ts

// ❌ Don't store context in structs
type BadService struct {
    ctx context.Context // Don't do this!
}

// ❌ Don't use string keys for context values
ctx = context.WithValue(ctx, "requestID", "123") // Collision risk!

// ❌ Don't pass nil context
processOrder(nil, orderID) // Use context.TODO() instead

// ❌ Don't use context for passing optional parameters
// Use functional options or config structs instead
ctx = context.WithValue(ctx, "retries", 3) // Bad!

Context Cause (Go 1.20+)

Go 1.20 introduced context.WithCancelCause for better error context:
func main() {
    ctx, cancel := context.WithCancelCause(context.Background())
    
    go func() {
        if err := doWork(ctx); err != nil {
            cancel(fmt.Errorf("work failed: %w", err))
        }
    }()
    
    <-ctx.Done()
    
    // Get the cause of cancellation
    cause := context.Cause(ctx)
    fmt.Println("Cancelled because:", cause)
}

AfterFunc (Go 1.21+)

Go 1.21 added context.AfterFunc to schedule cleanup:
func processWithCleanup(ctx context.Context) {
    resource := acquireResource()
    
    // Schedule cleanup when context is done
    stop := context.AfterFunc(ctx, func() {
        resource.Release()
        log.Println("Resource released due to context cancellation")
    })
    
    // If we finish normally, stop the scheduled cleanup
    defer stop()
    
    // Do work...
    useResource(resource)
    
    // Normal cleanup
    resource.Release()
}

Real-World Example: HTTP Service

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type OrderService struct {
    db     *sql.DB
    cache  *redis.Client
    notify *NotificationService
}

func (s *OrderService) CreateOrder(ctx context.Context, order *Order) error {
    // Add timeout for the entire operation
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // Step 1: Validate inventory (5s timeout)
    inventoryCtx, _ := context.WithTimeout(ctx, 5*time.Second)
    if err := s.validateInventory(inventoryCtx, order); err != nil {
        return fmt.Errorf("inventory validation failed: %w", err)
    }
    
    // Step 2: Process payment (10s timeout)
    paymentCtx, _ := context.WithTimeout(ctx, 10*time.Second)
    if err := s.processPayment(paymentCtx, order); err != nil {
        return fmt.Errorf("payment failed: %w", err)
    }
    
    // Step 3: Save to database
    if err := s.saveOrder(ctx, order); err != nil {
        // Payment succeeded but save failed - need compensation
        s.refundPayment(context.Background(), order) // Use background for cleanup
        return fmt.Errorf("failed to save order: %w", err)
    }
    
    // Step 4: Send notification (non-critical, don't fail the order)
    go s.notify.SendOrderConfirmation(context.Background(), order)
    
    return nil
}

func (s *OrderService) Handler(w http.ResponseWriter, r *http.Request) {
    // Extract request context
    ctx := r.Context()
    
    // Add request metadata
    ctx = context.WithValue(ctx, requestIDKey{}, r.Header.Get("X-Request-ID"))
    
    var order Order
    if err := json.NewDecoder(r.Body).Decode(&order); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }
    
    if err := s.CreateOrder(ctx, &order); err != nil {
        if ctx.Err() == context.Canceled {
            // Client disconnected
            log.Printf("Client disconnected during order creation")
            return
        }
        if ctx.Err() == context.DeadlineExceeded {
            http.Error(w, "Request timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(order)
}

Interview Questions

Resources are leaked! The goroutine managing the context’s timer continues running until the parent context is cancelled. Always defer cancel() immediately after creating a cancellable context.
No, contexts are immutable. WithValue returns a new context with the added value. The original context is unchanged.
Functionally identical, but semantically different:
  • Background(): Use at the top of the call chain (main, init, tests)
  • TODO(): Temporary placeholder when you’re not sure which context to use
Generally no. Context should flow through your program via function parameters. Storing in structs can lead to stale contexts and unclear lifecycle management.
When a parent context is cancelled, all child contexts are also cancelled. The cancellation is detected by receiving from ctx.Done(). Child context cancellation doesn’t affect the parent.

Summary

FunctionPurpose
context.Background()Root context for main, init, tests
context.TODO()Placeholder when unsure
context.WithCancel()Manual cancellation
context.WithTimeout()Cancel after duration
context.WithDeadline()Cancel at specific time
context.WithValue()Attach request-scoped data
context.WithCancelCause()Cancellation with reason (Go 1.20+)
context.AfterFunc()Schedule cleanup (Go 1.21+)