For a while I treated context.Context as a thing you grabbed when an HTTP handler needed cancellation, used once, and forgot. Then I had to add request timeouts to a chain of functions that did not take a context at all, and learned the lesson Go has been quietly trying to teach me: the context goes through everything, or it goes through nothing useful.
The signature convention is the whole discipline. It is the first argument, it is called ctx, and you do not stash it in a struct.
func (s *Store) Fetch(ctx context.Context, id string) (*Record, error) {
return s.db.QueryRowContext(ctx, query, id).Scan(&r.Name)
}
The friction is real when you retrofit it. Adding ctx to one function means adding it to its caller, and its caller's caller, all the way up to wherever the request actually starts. It feels like vandalism, touching twenty files to thread one argument. But that thread is the cancellation signal, and a chain that drops it anywhere is a chain that cannot be cancelled past that point. Either it reaches the database call or the timeout you set up top is a polite suggestion the bottom never hears. So you thread it all the way through, grumbling, and then it works.