Context abuse

April 10, 2025

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.
func Connect(host string, port int) (Connection, error) {

not this:

// Connect connects to the default service on the default port. If ctx contains
// a host or port value, those are used instead.
func Connect(ctx context.Context) (Connection, error) {

But what about request-scoped data that happens to be optional, such as an authenticated user id?

This is a clear example of something that is request-scoped, so is appropriate to include in a context. In fact, here’s a (non-exhaustive) list of data I’ll typically include in a context:

  • Authenticated user name or id
  • A unique request ID
  • (Rarely) User-specific configuration, to prevent repeated lookups of the same data
  • Response information for logging (this one is a bit complicated, and I’ll discuss it more later)

Now let’s discuss something more contentious: Should we pass user-scoped service objects via context? The two most common examples I see are a logger and database handle, but the same principle could apply for any service that depends on user context (i.e. you log into a third-party API as the user making the request).

By way of example, let’s imagine your service uses a PostgreSQL database, and each aplication user is mapped to a Postgres role, for security purposes. When a connection is established to your web service, you open a new PostgreSQL connection to the appropriate role, then add that connection to your context value. Then your handlers can pull that database object from the context to make its request.

Now the question is: Is this a good or bad idea? Is this object “request scoped data”, or is it an “optional parameter”?

Well, neither, really. It’s certianly not an optional parameter. But it’s not really “data”, either. It’s a request-scoped “access object”, if you will. So what rule do we apply?

Hit reply and let me know your thoughts (if you’d rather not be quoted in a future email, just say so in your reply). This email is already long enough, so I’ll share mine in the next email.


Share this

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

Unsure? Browse the archive .

Related Content


context.WithValue

func WithValue func WithValue(parent Context, key, val any) Context WithValue returns a derived context that points to the parent Context. In the derived context, the value associated with key is val. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context.


Context key/value utilities

So we’ve finally settled on a suitable type for our context key types: string, or maybe a non-empty struct. We also understand that to guard against key collisions, we need our custom type to be un-exported. type contextKey string const KeyUserID = contextKey("user_id") Now code anywhere in your project can use the context key without fear of collision. Done! Right? I’m sure you know the fact that I’m asking means there’s more we can do.


Context key type: Final recommendation

First, a correction! An astute reader pointed out that I made a small mistake in my post on June, 10, with regard to string context keys. My code example showed: type contextKey string const ( KeyUserID = "user_id" KeyTransactionID = "transaction_id" KeyMessageID = "message_id" ) But it should have been: type contextKey string const ( KeyUserID contextKey = "user_id" KeyTransactionID contextKey = "transaction_id" KeyMessageID contextKey = "message_id" ) This is a nasty kind of bug, because the code will continue to work as expected—just without any protection from key collisions!

Get daily content like this in your inbox!

Subscribe