Using context values in Go appears, at first sight, very trivial and easily understood. There are pitfalls, though. For instance, what type to use for the key?
❯ go doc context WithValue
package context // import "context"
func WithValue(parent Context, key, val any) Context
WithValue returns a copy of parent in which 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.
Users of WithValue should define their own types for keys. To avoid
allocating when assigning to an interface{}, context keys often have
concrete type struct{}. Alternatively, exported context key variables'
static type should be a pointer or interface.
This whole article exists because of this:
Users of WithValue should define their own types for keys.
OK, but what would that type be? It should NOT be a string, but its underlying type could be one. In fact, over the years, I’ve used an internal string type with a value equal to the full package name:
// ctxt is a dummy type for storing context values.
//
// For more information see the documentation of context.WithValue.
type ctxt string
var ctxkey ctxt = "github.com/org/project/pkg"
Now &ctxkey is what we use as context key as suggested by the documentation
in order to avoid useless allocations. Often I would introduce public functions
to take or store their expected value, without dealing with keys, mostly for
testing. Suppose all this package needs a ctx type for is to store values of
type T:
// FromContext extracts a value of type T from the given context, returning its
// zero value if not present.
func FromContext(ctx context.Context) T {
v, ok := ctx.Value(&ctxkey).(T)
if !ok {
v = NewT()
}
return v
}
// WithValue returns a copy of parent with stored T inside.
func WithValue(parent context.Context, t T) context.Context {
return context.WithValue(parent, &ctxkey, t)
}
pkg.FromContext(ctx) takes a context and returns the pkg.T stored inside,
if any, for instance.
The point to store the full package name is merely to ease debugging and/or
tracing. This string is allocated only once, and we are using a pointer to it,
in order not to allocate once per assigning that key to interface{}. If we
want to get fanatical about that, the type of ctxt might be an empty struct:
// ctxkey is a dummy type for storing context values.
//
// For more information see the documentation of context.WithValue.
type ctxkey struct{}
Now we no longer need to use a pointer to ctxkey and just directly pass
ctxkey{} instead, because a value of struct{} never allocates as its size
is 0 anyways. When debugging you would still see the type of ctxkey, which (as
with any type) contains the full path to it.
I have no memory which tool/use-case didn’t work well with struct{} so I’ve
started using a string in the first place, but I’m back to using struct{}
again these days.
What about generics?
Yes, of course. Go has generics now for quite some time, why not sprinkling some of it to solve that? We could. In fact, there is a proposal that is placed on hold (at the moment of writing this) in order to give the community time to play more with generics: https://github.com/golang/go/issues/49189
If you have tens of packages storing stuff in contexts and are pretty sure that
this makes sense, might as well steal the proposed Key[T] from there.