Go context Package

Propagate cancellation, deadlines, and request-scoped values across goroutines and API boundaries.

Background ยท TODO WithCancel WithTimeout WithDeadline WithValue Done ยท Err
๐ŸŒฑ

Root Contexts

โ„น๏ธ
Every context tree starts from one of two root values. You never create a context from scratch โ€” you always derive from a root or from another context.
context.Background Background
// Background โ€” the top-level root context
// Never cancelled, has no deadline, no values
// Use as the starting point in main, tests, and
// at the top of incoming request handlers
ctx := context.Background()

// Typical patterns
func main() {
    ctx := context.Background()
    run(ctx)
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // already set by net/http
    doWork(ctx)
}
context.TODO TODO
// TODO โ€” a placeholder when you're not yet sure
// which context to use, or the calling function
// doesn't accept one yet
ctx := context.TODO()

// Use TODO to signal "this needs to be wired up"
// It behaves identically to Background at runtime
// but communicates intent to future readers

// Tools like staticcheck can detect TODO contexts
// and remind you to replace them

// Rule of thumb:
// - Background: you know this is the right root
// - TODO: you know this is temporary
โŒ

WithCancel

โš ๏ธ
Always call the cancel function โ€” even if work completes successfully before cancellation. Failing to call it leaks resources until the parent context is cancelled.
WithCancel โ€” cancel on demand WithCancel
ctx, cancel := context.WithCancel(context.Background())
defer cancel()  // always call cancel to free resources

go func() {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker stopped:", ctx.Err())
            // ctx.Err() == context.Canceled
            return
        default:
            doWork()
        }
    }
}()

// Cancel all derived goroutines when done
time.Sleep(2 * time.Second)
cancel()  // signals ctx.Done() to close
Cancellation propagates to all children Propagation
parent, cancelParent := context.WithCancel(context.Background())
defer cancelParent()

child1, cancel1 := context.WithCancel(parent)
defer cancel1()

child2, cancel2 := context.WithCancel(parent)
defer cancel2()

// Cancelling the parent cancels child1 and child2 too
cancelParent()

// child1.Err() == context.Canceled
// child2.Err() == context.Canceled

// Cancelling a child does NOT affect the parent or siblings
cancel1()
// parent.Err() == nil  (still alive)
// child2.Err() == nil  (still alive)
โฑ

WithTimeout & WithDeadline

๐Ÿ’ก
WithTimeout(ctx, d) is shorthand for WithDeadline(ctx, time.Now().Add(d)). Use WithTimeout when you think in durations ("5 seconds"), WithDeadline when you think in absolute times ("until 3pm").
WithTimeout WithTimeout
ctx, cancel := context.WithTimeout(
    context.Background(),
    5*time.Second,
)
defer cancel()  // still call cancel to release early

resp, err := http.Get("https://example.com")
// if request takes > 5s, ctx is cancelled
// and err wraps context.DeadlineExceeded

// Check for timeout specifically
if errors.Is(err, context.DeadlineExceeded) {
    log.Println("request timed out")
}
WithDeadline WithDeadline
// WithDeadline โ€” cancel at an absolute time
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(
    context.Background(),
    deadline,
)
defer cancel()

// Check when the context will expire
dl, ok := ctx.Deadline()
if ok {
    fmt.Println("expires at:", dl)
    fmt.Println("time left:", time.Until(dl))
}
Timeout with database / HTTP โ€” real-world pattern Pattern
func fetchUser(ctx context.Context, id int) (*User, error) {
    // Apply a local timeout on top of whatever deadline the caller set
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    var user User
    err := db.QueryRowContext(ctx,
        "SELECT * FROM users WHERE id = $1", id,
    ).Scan(&user.ID, &user.Name)
    if err != nil {
        return nil, fmt.Errorf("fetchUser: %w", err)
    }
    return &user, nil
}

// Whichever deadline is sooner โ€” the caller's or the local 3s โ€” wins
๐Ÿ“ก

Context Methods

The four methods on context.Context Interface
type Context interface {
    // Done returns a channel that closes when the context is cancelled
    // or times out. Returns nil for Background and TODO (never closes).
    Done() <-chan struct{}

    // Err returns nil while the context is active.
    // After Done closes: context.Canceled or context.DeadlineExceeded
    Err() error

    // Deadline returns the expiry time if one is set.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value for a key set via WithValue, or nil.
    Value(key any) any
}
Listening on Done in a select Done
func processItems(ctx context.Context, items <-chan Item) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()  // Canceled or DeadlineExceeded

        case item, ok := <-items:
            if !ok {
                return nil  // channel closed normally
            }
            if err := process(item); err != nil {
                return err
            }
        }
    }
}

// Check context before expensive operations
func doWork(ctx context.Context) error {
    if err := ctx.Err(); err != nil {
        return err  // bail early, context already done
    }
    // proceed with work...
    return nil
}
context.Canceled vs DeadlineExceeded Errors
// Two sentinel errors returned by Err()
context.Canceled         // cancel() was called
context.DeadlineExceeded // timeout/deadline passed

// Distinguish with errors.Is
switch ctx.Err() {
case context.Canceled:
    log.Println("cancelled by caller")
case context.DeadlineExceeded:
    log.Println("timed out")
}
Checking remaining time Deadline
dl, ok := ctx.Deadline()
if !ok {
    // no deadline set โ€” run as long as needed
}

remaining := time.Until(dl)
if remaining < 100*time.Millisecond {
    return fmt.Errorf("not enough time to proceed")
}

// Pass a tighter deadline to a sub-call
subCtx, cancel := context.WithDeadline(ctx,
    dl.Add(-50*time.Millisecond), // 50ms buffer
)
defer cancel()
๐Ÿ“ฆ

WithValue

โš ๏ธ
Use WithValue only for request-scoped data that crosses API boundaries โ€” things like trace IDs, auth tokens, or locale. Don't use it as a shortcut for passing optional function parameters.
WithValue โ€” typed keys prevent collisions WithValue
// Always use an unexported custom type as the key
// so no other package can accidentally read or overwrite it
type contextKey string

const (
    keyTraceID  contextKey = "traceID"
    keyUserID   contextKey = "userID"
)

// Store a value
ctx := context.WithValue(r.Context(), keyTraceID, "abc-123")
ctx  = context.WithValue(ctx, keyUserID, 42)

// Retrieve a value โ€” returns any, so assert the type
traceID, ok := ctx.Value(keyTraceID).(string)
if !ok {
    traceID = "unknown"
}

userID, _ := ctx.Value(keyUserID).(int)
Package-level helper functions โ€” the idiomatic pattern Pattern
// Define the key type privately in your package
type traceKey struct{}

// Expose typed helper functions instead of the raw key
func WithTraceID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, traceKey{}, id)
}

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

// Usage โ€” callers never touch the key directly
ctx = WithTraceID(ctx, "req-789")
if id, ok := TraceID(ctx); ok {
    log.Printf("trace=%s", id)
}
๐Ÿ“

Common Patterns

HTTP server โ€” context flows from request to dependencies HTTP
func handler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is cancelled when the client disconnects
    ctx := r.Context()

    // Add a per-request timeout
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Attach trace ID from incoming header
    ctx = WithTraceID(ctx, r.Header.Get("X-Trace-ID"))

    // Pass ctx to every downstream call
    user, err := fetchUser(ctx, 123)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}
Graceful shutdown with os.Signal Shutdown
func main() {
    ctx, stop := signal.NotifyContext(
        context.Background(),
        syscall.SIGINT, syscall.SIGTERM,
    )
    defer stop()

    server := &http.Server{Addr: ":8080"}

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Block until SIGINT or SIGTERM
    <-ctx.Done()
    log.Println("shutting down...")

    shutdownCtx, cancel := context.WithTimeout(
        context.Background(), 10*time.Second,
    )
    defer cancel()
    server.Shutdown(shutdownCtx)
}
Fan-out โ€” cancel all workers when the first fails Fan-out
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    results := make([]string, len(urls))
    errCh   := make(chan error, len(urls))
    var wg sync.WaitGroup

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            body, err := fetch(ctx, url)
            if err != nil {
                cancel()         // cancel all siblings
                errCh <- err
                return
            }
            results[i] = body
        }(i, url)
    }

    wg.Wait()
    close(errCh)
    return results, <-errCh
}
๐Ÿ“‹

Quick Reference

Function / ValueReturnsDescription
context.Background()ContextRoot context; never cancelled, no deadline, no values
context.TODO()ContextPlaceholder root; signals "this should be wired up later"
context.WithCancel(parent)Context, CancelFuncNew context cancelled when parent is or cancel() is called
context.WithTimeout(parent, d)Context, CancelFuncCancelled after duration d or when parent is cancelled
context.WithDeadline(parent, t)Context, CancelFuncCancelled at absolute time t or when parent is cancelled
context.WithValue(parent, k, v)ContextNew context carrying value v under key k
ctx.Done()<-chan struct{}Closed when context is cancelled or times out; nil for Background/TODO
ctx.Err()errornil while active; Canceled or DeadlineExceeded after Done closes
ctx.Deadline()time.Time, boolReturns the deadline and whether one is set
ctx.Value(key)anyReturns value stored under key, or nil
context.CancelederrorSentinel: cancel() was called
context.DeadlineExceedederrorSentinel: deadline or timeout passed
signal.NotifyContext(ctx, sigs...)Context, StopFuncCancel context on OS signals (os/signal package)