18. context: cancellation, deadlines, and request-scoped values
18.1 Why context exists
A single incoming HTTP request might fan out to a database query, two
downstream API calls, and a cache lookup. If the client disconnects, or a
deadline expires, every one of those in-flight operations needs to hear
about it now — not run to completion pointlessly. context.Context is
the value threaded through every layer of a call stack to carry exactly
that signal.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // ALWAYS defer cancel, even if you expect the timeout to fire first
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("request timed out")
}
return err
}
defer resp.Body.Close()
return nil
18.2 The four context constructors
| Constructor | Cancels when |
|---|---|
context.Background() |
Never — the root of every context tree |
context.WithCancel(parent) |
You call the returned cancel() |
context.WithTimeout(parent, d) |
Duration d elapses, or manual cancel |
context.WithDeadline(parent, t) |
Wall-clock time t is reached |
Cancellation propagates downward only: cancelling a parent cancels every context derived from it; a child can never cancel its parent.
18.3 Respecting cancellation in your own code
Context does nothing by itself — code has to actually check ctx.Done():
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return // stop working, ctx.Err() explains why
case job, ok := <-jobs:
if !ok {
return
}
process(job)
}
}
}
18.4 Context values — use sparingly
context.WithValue carries request-scoped metadata (a request ID, an
authenticated user) across API boundaries that can't take an explicit
parameter. It is not a substitute for passing real parameters — if a
function needs a value to do its job, put it in the signature; reserve
context values for cross-cutting concerns like tracing IDs.
type requestIDKey struct{}
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func requestIDFrom(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey{}).(string); ok {
return id
}
return ""
}
Using an unexported struct type as the key (instead of a string)
prevents collisions with context keys defined in other packages.
18.5 Real-world example
Week 2's concurrent URL checker cancels all remaining checks if the total
job takes longer than 30 seconds — that's context.WithTimeout wrapping
the whole batch, passed down into every worker's individual HTTP call, so
one shared deadline governs a fan-out of N concurrent requests without any
manual timer bookkeeping.
Applied in the series: context cancels dials and probes in the TCP/IP guide's dialcheck and stops streaming handlers on disconnect in the HTTPS guide's SSE lab.