For a long time I treated context.Context as ceremony. Every function grew a ctx context.Context first argument, I passed it down dutifully, and I wasn't sure what it bought me beyond more typing. It clicked the day a request got cancelled mid-flight and I watched, in a profiler, a database query carry on running for thirty seconds after the client had hung up. Nobody was waiting for the result. The work was pure waste, and it was waste I'd written.
Context is the wire that carries "stop" from the top of a call stack to the bottom. When an HTTP request is cancelled, its context is cancelled, and anything that respects that context stops too. But only if you actually thread it through. A ctx that gets dropped halfway down, replaced with context.Background() because some function in the middle didn't take one, is a wire that's been cut. Everything below the cut keeps running.
The rule I settled on is boring and works: context is the first argument of any function that does I/O or blocks, and you pass the one you were given. You don't store it in a struct, you don't make a new background one to "be safe", you pass the live one down.
The part that took longest to internalise is that select on ctx.Done() is how you actually honour cancellation in your own blocking code. The standard library does it for you on database and HTTP calls, but the moment you write your own loop or your own channel wait, you have to check it yourself:
func worker(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case j, ok := <-jobs:
if !ok {
return nil
}
process(j)
}
}
}
Leave out the ctx.Done() case and that goroutine never learns the request died. It sits there blocked on the channel forever. Do that in a request handler and you've written a goroutine leak that only shows up under load, weeks later, as memory that climbs and never falls.
The other thing worth saying plainly: context.WithTimeout returns a cancel function, and you defer cancel() even when the timeout fires on its own. Forgetting it doesn't break correctness, but it leaks the timer until the deadline passes. go vet will tell you off for it now, which is how I learned.
It stopped feeling like ceremony once I saw it as plumbing for cancellation rather than a bag for passing values. Thread it through honestly, check Done() in your own waits, and the cancelled work actually stops. That's the whole point, and it's worth the extra argument.