For a long time context.Context was a parameter I passed because the linter wanted me to. I'd take it in at the HTTP handler, ignore it, and call context.Background() two layers down when a function needed one. It compiled. It also meant that when a client hung up, my service carried on doing the work anyway, querying a database for an answer nobody was waiting for.
The point of context is propagation. One request gets one context, and it threads down the entire call stack so that when the top cancels, everything beneath it learns about it. The moment you call context.Background() mid-stack, you've cut the wire.
the rule that fixed my code
Context is the first argument, it comes from above, and you never store it on a struct. That's it. Once I committed to that, a lot of vague flakiness went away.
func (s *Service) Lookup(ctx context.Context, id string) (*User, error) {
row := s.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
// ...
}
The QueryRowContext rather than QueryRow is the whole game. Pass the same ctx you were handed, and a cancelled request stops the query instead of letting it run to completion in a void.
deadlines and the shutdown case
Two patterns earned their keep. The first is a per-call deadline, so a slow dependency can't hold a request open forever:
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
That defer cancel() is not optional. Forget it and you leak the timer, and go vet will, quite rightly, shout at you. The second pattern is graceful shutdown. When the process gets a SIGTERM, I cancel a root context, and every in-flight worker that's been threading it through sees the cancellation and winds down cleanly rather than being killed mid-write.
what I stopped doing
I stopped stuffing things into context values. context.WithValue is there, and it's the right tool for request-scoped metadata like a trace ID, but I'd been smuggling actual dependencies through it, which is just dependency injection wearing a disguise and skipping the type checker. Those belong in function arguments or on the struct, where the compiler can see them.
None of this is clever. It's mostly the discipline of threading one value honestly from top to bottom instead of papering over the gap with Background(). The reward is a service that knows when to stop, which under load turns out to matter rather a lot.