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.