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


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.

Get daily content like this in your inbox!

Subscribe