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.