CHAPTER 26 · CONCURRENCY

Context: Cancellation & Deadlines

context.Context is the most important type in modern Go programs that do anything networked. It threads three concerns through your call stack: cancellation signals, deadlines/timeouts, and request-scoped values.

Learning objectives

  • Explain why Go added context.
  • Create a context with cancel, timeout, or deadline.
  • Respect a context inside your functions by listening on ctx.Done().
  • Attach request-scoped values, and know when not to.
  • Pass context as the first argument through your call stack.

Why context exists

Imagine a web request that kicks off three database calls, two external HTTP requests, and a log write. If the user cancels the HTTP connection, every goroutine doing that work should stop; otherwise you waste CPU, memory, and database connections.

Context is how Go propagates "stop what you're doing" down a call tree.

The Context interface

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

The important one is Done(): a channel that closes when the context is cancelled or expired. You select on it alongside your actual work.

context.Background() and context.TODO()

ctx := context.Background()   // root context, never cancelled
ctx := context.TODO()         // "I don't know what to pass yet", semantically equivalent, signals intent

Every context chain starts from one of these at main (or an HTTP handler, or a test). You derive child contexts from them.

context.WithCancel

ctx, cancel := context.WithCancel(context.Background())
defer cancel()       // always call cancel to release resources

go func() {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("work done")
    case <-ctx.Done():
        fmt.Println("cancelled:", ctx.Err())
    }
}()

cancel()             // signal cancellation

Always defer cancel(), even if you think the context will expire on its own. It's cheap and prevents leaks.

WithTimeout and WithDeadline

// Fire cancellation 2 seconds from now
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

// Or: fire at a specific time
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Minute))
defer cancel()

The child context is cancelled when (a) the timeout/deadline fires, or (b) the parent is cancelled, or (c) you call cancel() explicitly. Whichever happens first.

WithValue

type userIDKey struct{}         // unexported key type avoids collisions

ctx := context.WithValue(ctx, userIDKey{}, 42)

if v := ctx.Value(userIDKey{}); v != nil {
    fmt.Println("user:", v.(int))
}
!
Don't put business data here ctx.Value is for request-scoped values like trace IDs, auth tokens, and logger handles, values that cross API boundaries but aren't part of the function's intent. Never use it to pass regular arguments; function signatures are clearer.

HTTP request context

Every *http.Request carries a context:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()           // cancelled when client disconnects
    result, err := queryDB(ctx, id)
    // ...
}

Pass r.Context() through to every operation that could outlive the request. When the client disconnects, everything cascades to cancellation.

Rules & conventions

  • First parameter of the function. Always name it ctx: func DoThing(ctx context.Context, ...) error.
  • Don't store it in a struct unless the struct IS a request.
  • Don't pass nil context. Use context.TODO() if you don't have one.
  • Always call cancel, even in timeout/deadline contexts, usually via defer.
  • Check ctx.Done() in any long-running or blocking operation.

Check your understanding

Practice exercises

EXERCISE 1

Context-aware sleep

Write sleep(ctx context.Context, d time.Duration) error that sleeps for d, unless the context is cancelled first, in which case it returns ctx.Err().

Show solution
func sleep(ctx context.Context, d time.Duration) error {
    select {
    case <-time.After(d):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    if err := sleep(ctx, 2*time.Second); err != nil {
        fmt.Println("interrupted:", err)    // context deadline exceeded
    }
}

Further reading

Context in hand.