Context values and type safety

April 16, 2025

Last week I asked whether or not it’s a good idea to pass things like a user-scoped database handle or logger via a context value.

Before I provide my direct answer, I want to take a short detour…

Go is a (mostly) strictly-typed language. This gives us certain guarantees. When we have a variable of type int, we know it doesn’t contain the value "cow", because that’s not an integer.

Functions, in Go, are strictly typed. func (int) requires an integer argument. Trying to pass "cow" to such a function will fail at compilation time.

This is a good thing, in most contexts. For one thing, it saves us from having to do a lot of runtime validation. But perhaps more important, it serves as clear documentation about what is both expected, and guaranteed, for a type, or a function.

As the consumer of an API with a function func (int), I can make certain assumptions about what the function does and does not do. Contrast this to a function in a dynamic language, like, say, JavaScript, which has neither strict types, nor strict function signatures. In such a language, I need to look at the function body itself to understand how it uses its argument(s), and indeed, how many arguments it even accepts.

Of course dynamic languages are extremely flexible, as well, which is great for certain things.

Go gives us some of that flexibility with the empty interface (interface{} aka any). And that is the type used for context values. Context values are effectively a map of any values. You may be guessing where I’m going with this now… This means that values in a context are:

  • Extremely flexible
  • Absent strict type guarantees
  • Don’t provide API documentation

Of course the flexibility can be a benefit in some cases. But assuming we appreciate Go’s type safety (and I certainly do!), the other two are pretty serious drawbacks much of the time.

Can we overcome those drawbacks? I’ll be discussing that next.


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