For a long time context.Context felt like ceremony. Every function grew a ctx context.Context first argument, I dutifully passed it down, and I could not have told you what it bought me. So I did the lazy thing in a few hot paths and reached for context.Background() whenever threading the real one felt tedious.
Then a handler started leaking goroutines. A request would come in, fan out to three downstream calls, and the client would hang up after a second. The downstream calls had no idea anyone had left. They sat there for their full thirty-second timeout, holding connections, while new requests piled in behind them. The fix was the thing I had been skipping: pass the request's context down so cancellation actually propagates.
func fetch(ctx context.Context, id string) (*Item, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url(id), nil)
return do(req)
}
When the client disconnects, the server cancels the request context, the cancellation walks down the tree, and every in-flight call gives up at once. The goroutines drain instead of accumulating. That is the whole point, and I had been opting out of it one Background() at a time.
The rule I follow now is boring and I no longer fight it: ctx is the first parameter, you never store it in a struct, and you only mint a fresh one at a genuine boundary like a background job. It is not ceremony. It is the wiring that lets a cancelled request actually stop doing work.