What is the context package used for, and how does cancellation propagate?

5 minintermediategocontextcancellationconcurrency

Quick Answer

context.Context carries deadlines, cancellation signals, and request-scoped values across API boundaries and between goroutines. A context.Context is typically the first parameter of any function that does I/O or might run long, by convention named ctx. context.WithCancel, context.WithTimeout, and context.WithDeadline derive a child context that can be cancelled explicitly or automatically; cancelling a parent context automatically cancels every context derived from it, which is how cancellation propagates down a whole call tree at once. Functions that respect cancellation select on ctx.Done() alongside their actual work, so a single cancel call at the top can stop an entire tree of goroutines without each one needing its own separate signal.

Detailed Answer

context.Context is how Go plumbs "please stop" and "you have until X" through a call chain without every function needing bespoke cancellation logic.

Basic timeout usage

func fetchData(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
data, err := fetchData(ctx, "https://example.com")

If the request takes longer than 3 seconds, the context's Done() channel closes, and http.NewRequestWithContext makes the underlying request abort with a context-deadline error.

Propagation through a call tree

ctx, cancel := context.WithCancel(context.Background())

go worker(ctx)      // derived context
go anotherWorker(ctx)

cancel()  // both worker and anotherWorker see ctx.Done() close, and can exit

Calling cancel() once cancels every goroutine that was handed this same ctx (or any context derived further from it), which is what makes context-based cancellation scale to a whole tree of concurrent work from a single call.

A function that respects cancellation

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("cancelled:", ctx.Err())
            return
        case <-time.After(500 * time.Millisecond):
            fmt.Println("still working")
        }
    }
}

The convention to follow

Always accept ctx context.Context as the first parameter, never store it inside a struct, and always call the cancel function returned by WithCancel/WithTimeout/WithDeadline (usually via defer cancel()) to release the context's resources even if it wasn't triggered by a timeout.