Overview
…
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. This is discussed further in https://go.dev/blog/context-and-structs.
We’ve already discussed how the proliferation of the ctx context.Context
argument can start to look more cluttered than my desk on a Friday afternoon. One might naturally be tempted to tidy up that “clutter” by stashing away a context.Context
value in a struct, for repeated use.
Please, don’t do that.
The article linked above, Contexts and structs, goes into details as to why not to do this. But here’s the TL;DR; version, for those who can’t be bothered to read the post (really, you should go read the post. It’s not long.)
- One context per function call allows for granular control.
- A context in a struct can be confusing—it’s often unclear which function call(s) the context does or does not apply to.
The article also offers an exception to the rule: Backward compatibility.
The example provided is the net/http
package, which opted for storing a context.Context
in the http.Request
struct, rather than adding a DoWithContext
function.
This means that net/http
is probably the most heavily used exception to this rule of “do not store Contexts inside a struct type.” Please, don’t use this as an excuse for storing contexts in structs in your new code!
And even if you’re retrofitting old code to now be context aware, and you must maintain backward compatibility, you should strongly consider the alternative approach instead, the one taken by the database/sql
package: Simply duplicate your existing functions, to add context:
func (db *DB) Query(query string, args ...any) (*Rows, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
I’ll also offer one additional exception. It’s also a rare one, but an acceptable one, in my opinion: The builder pattern.
The builder pattern isn’t terribly common in Go. There are usually better alternatives. But occasionally you’ll see the builder pattern, and when you do, storing a context in a struct that is used to hold the built-up arguments can make sense. For example:
results, err = thingy.New().
Context(ctx).
Limit(15).
City("New York").
All()
In practice, this is the same as passing a context to an individual function, but in implementation, it would typically involve storing the context in a struct. (I’ll leave the actual backing implementation as an exercise for the reader.)