Skip to main content
The context package defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

The Context Interface

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Creating Contexts

Background and TODO

import "context"

// Background returns a non-nil, empty Context
// Used as the top-level Context for incoming requests
ctx := context.Background()

// TODO returns a non-nil, empty Context
// Use when it's unclear which Context to use or not yet available
ctx := context.TODO()

WithCancel

Create a context that can be manually cancelled.
import (
    "context"
    "fmt"
    "time"
)

func withCancelExample() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // Always call cancel to release resources
    
    go func() {
        time.Sleep(2 * time.Second)
        cancel() // Cancel the context
    }()
    
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("Operation completed")
    case <-ctx.Done():
        fmt.Println("Operation cancelled:", ctx.Err())
    }
}

WithTimeout and WithDeadline

Create contexts that cancel automatically after a duration or at a specific time.
// WithTimeout: cancel after duration
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// WithDeadline: cancel at specific time
deadline := time.Now().Add(10 * time.Second)
ctx, cancel = context.WithDeadline(context.Background(), deadline)
defer cancel()

WithValue

Carry request-scoped data through the call chain.
type key string

const userKey key = "user"

func withValueExample() {
    ctx := context.WithValue(context.Background(), userKey, "alice")
    
    // Retrieve value
    if user, ok := ctx.Value(userKey).(string); ok {
        fmt.Printf("User: %s\n", user)
    }
}

Practical Examples

HTTP Request with Timeout

import (
    "context"
    "io"
    "net/http"
    "time"
)

func makeRequest(url string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return err
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    _, err = io.ReadAll(resp.Body)
    return err
}

Database Query with Context

import (
    "context"
    "database/sql"
    "time"
)

func queryDatabase(db *sql.DB, userID int) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    var name string
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", userID).Scan(&name)
    if err != nil {
        return err
    }
    
    fmt.Printf("User: %s\n", name)
    return nil
}

Worker Pool with Cancellation

func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d cancelled\n", id)
            return
        case job, ok := <-jobs:
            if !ok {
                return
            }
            results <- job * 2
        }
    }
}

func workerPool() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    // Start workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, jobs, results)
    }
    
    // Send jobs
    for i := 1; i <= 5; i++ {
        jobs <- i
    }
    close(jobs)
    
    // Get results or cancel
    for i := 1; i <= 5; i++ {
        select {
        case result := <-results:
            fmt.Printf("Result: %d\n", result)
        case <-time.After(1 * time.Second):
            cancel() // Cancel all workers
            return
        }
    }
}

Chained Context Operations

func processRequest(ctx context.Context) error {
    // Add timeout to incoming context
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // Add request ID to context
    ctx = context.WithValue(ctx, "requestID", "req-123")
    
    return performOperation(ctx)
}

func performOperation(ctx context.Context) error {
    select {
    case <-time.After(2 * time.Second):
        requestID := ctx.Value("requestID")
        fmt.Printf("Operation completed for %v\n", requestID)
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

WithCancelCause (Go 1.20+)

Provide a reason for cancellation.
import (
    "context"
    "errors"
    "fmt"
)

func withCancelCauseExample() {
    ctx, cancel := context.WithCancelCause(context.Background())
    
    go func() {
        time.Sleep(1 * time.Second)
        cancel(errors.New("operation timed out"))
    }()
    
    <-ctx.Done()
    fmt.Println("Context cancelled:", context.Cause(ctx))
}

Best Practices

1. Pass Context as First Parameter

// Good
func DoSomething(ctx context.Context, arg string) error {
    // ...
}

// Bad
func DoSomething(arg string, ctx context.Context) error {
    // ...
}

2. Don’t Store Context in Structs

// Bad
type Server struct {
    ctx context.Context
}

// Good - pass context to methods
type Server struct {}

func (s *Server) HandleRequest(ctx context.Context) error {
    // ...
}

3. Always Call Cancel Functions

func goodExample() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // Always defer cancel
    
    // Use ctx...
}

4. Don’t Pass nil Context

// Bad
DoSomething(nil, "arg")

// Good - use context.TODO if unsure
DoSomething(context.TODO(), "arg")

5. Use Typed Keys for Context Values

// Good - unexported type prevents collisions
type contextKey string

const userIDKey contextKey = "userID"

func SetUserID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, userIDKey, id)
}

func GetUserID(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(userIDKey).(string)
    return id, ok
}

Context Errors

var (
    Canceled         = errors.New("context canceled")
    DeadlineExceeded = errors.New("context deadline exceeded")
)

// Check error type
if err := ctx.Err(); err != nil {
    if errors.Is(err, context.Canceled) {
        // Context was cancelled
    } else if errors.Is(err, context.DeadlineExceeded) {
        // Timeout occurred
    }
}

When to Use Context

Use Context for:
  • Request timeouts and deadlines
  • Cancellation signals across goroutines
  • Request-scoped values (user ID, trace ID, etc.)
  • Propagating cancellation through call chains
Don’t Use Context for:
  • Optional function parameters
  • Storing application state
  • Passing values that should be function arguments
  • Long-lived background operations that shouldn’t be cancelled

Common Patterns

Graceful Shutdown

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    // Handle shutdown signals
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        <-sigChan
        cancel() // Cancel context on signal
    }()
    
    runServer(ctx)
}

func runServer(ctx context.Context) {
    // Server operations that respect ctx.Done()
}

Timeout with Retry

func retryWithTimeout(ctx context.Context, attempts int, delay time.Duration, fn func() error) error {
    for i := 0; i < attempts; i++ {
        if err := fn(); err == nil {
            return nil
        }
        
        select {
        case <-time.After(delay):
            // Continue to next attempt
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return fmt.Errorf("failed after %d attempts", attempts)
}

Build docs developers (and LLMs) love