For a while I treated context.Context as a thing you only needed when something explicitly asked for one, an HTTP handler here, a database call there. So I'd grab a context.Background() at the point of use and move on. It worked, right up until I needed to cancel a request mid-flight and discovered the cancellation had nowhere to flow, because I'd broken the chain in the middle.
The lesson that finally landed: context isn't a parameter you fetch, it's a parameter you pass on. If a function does work that could block (a network call, a query, anything that waits), it takes a ctx context.Context as its first argument and hands it down to whatever it calls. The whole point is that a cancellation or timeout at the top propagates all the way to the bottom without you wiring anything up.
func (s *Service) Fetch(ctx context.Context, id string) (*Item, error) {
row := s.db.QueryRowContext(ctx, query, id)
// ctx flows down; cancel up top, this query gets cancelled too
}
The rules I now follow without thinking: context first, always. Never store one in a struct. Don't pass nil; use context.TODO() if you genuinely don't have one yet, so it's greppable later. And context.Value is for request-scoped data that crosses API boundaries, like a trace ID, not for sneaking optional arguments past a function signature you couldn't be bothered to change. That last temptation is the one I still have to talk myself out of.