Context rules

March 27, 2025

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.)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


The Context API contract

Today we come to the core of the context package: The Context interface itself. type Context type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } A Context carries a deadline, a cancellation signal, and other values across API boundaries. Context’s methods may be called by multiple goroutines simultaneously. For clarity, I’ve removed all of the documentation for each of the interface methods in the above quote—we’ll get to those in following emails, and include those there.


Concurrent use of contexts

Overview … The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. Contexts are built using layers. Every time you call one of the context.With* functions, the orginal context is wrapped in a new layer. This is the “magic” that makes contexts concurrency safe by design–they’re never mutated once they’re created. ctx := context.Background() // Create an original context ctx, cancel = context.


Context abuse

We’re nearly through the overview of the context package, when we come across this seemlingly straightforward sentence: Overview … Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. But is that really so straight forward? Let’s consider some examples. Let’s first tackle what probably is straight-forward: For garden variety optional parameters, context is the wrong tool! Do this: // Connect connects to the service using the provided host and port, or the // default host and/or port if omitted.

Get daily content like this in your inbox!

Subscribe