So we’ve established that context keys should be of an unexported type in your package. That still leaves a lot of options. Which type is ideal? Today we’ll look at some options, and their pros and cons.
Before looking at specific types, let’s consider what sorts of things we want from such a type.
-
It must be comparable, or it won’t work as a key. This means we can’t use function or channel types, for example. These wouldn’t make for very natural keys anyway, so not likely much of a practical constraint.
-
It should probably use the least amount of memory necessary.
-
It should be readable, both in code, and when debugging (i.e. in logging or debugger output)
-
It should be easy to declare multiple keys in the same package, without conflict.
-
Optionally, it may be nice if it can be expressed as a
const
.
With these goals in mind, let’s now consider some possible types, starting with the empty struct for today:
struct{}
This is one option that many people won’t even consider off the bat, because it’s a bit obscure. But let’s talk about it. Does it meet our criteria?
It is comparable. It uses 0 memory, so it’s ideal for criteria #2, and in fact this is what makes it an attractive option. But it fails on all the other counts.
For an empty struct to work as a context key, you must define a new, distinct type for every key.
type keyOne struct{}
type keyTwo struct{}
ctx := context.WithValue(context.Background(), keyOne{}, "value1")
ctx = context.WithValue(ctx, keyTwo{}, "value2")
It’s not sufficient to do this:
type key struct{}
var keyOne = struct{}
var keyTwo = struct{}
or even this:
var keyOne = &struct{}
var keyTwo = &struct{}
Why not?
From the Go spec:
Pointers to distinct zero-size variables may or may not be equal.
So to make use of the empty struct (or any other zero-sized type) for context keys, we must rely on type information, and thus one type per key. This is cumbersome. And it makes debugging less straight forward.
Bottom line: Probably don’t use this option.