What is the context package used for, and how does cancellation propagate?
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.