Alternative to using context.Value
I love context.Context
. But I don’t like using context.Value(). Because it relies on using interface{}
, it requires the programmers to write a possibly panic-inducing type assertion like this:
func foo(ctx context.Context) {
v := ctx.Value(key).(*MyThing) // eek
...
}
Obviously it’s possible to avoid the panic by using a two return value form of type assertion, but that just adds more code for each function that requires the use of this value.
Let’s step back and think what context.WithValue
is supposed to offer us.
I believe that WithValue
and Value
were created for bundling data to a particular set of processing routines. To that end, you are (MOSTLY) expecting tight coupling of the data associated to that context.Context
and the routines.
For example, you cannot expect a function call to modify the registered value of a context.Context
object when you pass one to it:
func mutatingFunc(ctx context.Context) {
ctx = context.WithValue(ctx, key, ...)
...
}ctx := context.WithValue(context.Background(), key, ...)
mutatingFunc(ctx)ctx.Value(key) // "mutatingFunc" can't influence this value
The only way to change the registered value is to “exchange” the context.Context
object:
func mutatingFunc(ctx context.Context) context.Context {
return context.WithValue(ctx, key, ...)
}ctx = mutatingFunc(ctx) // ctx is now replaced with a new context
ctx.Value(key) // value is what "mutatingFunc" set to it
This means changes are “local” to the scope where the context.Context
was created. You can’t interact with the upper scope. So its use is pretty limited.
Instead, I like to just make a wrapper for context.Context
, with the correct type and struct field. Here’s an example:
type myCtx struct {
context.Context
value *MyThing
}func EntryPoint(ctx context.Context) {
myctx := &myCtx{ // Create your own context.Context wrapper
Context: ctx,
value: ... // whatever local value
}
foo(myctx)
}func foo(ctx *myCtx) { // expect the concret type, not the interface
v := ctx.value // Safe!
...
}
There are two nice properties that this pattern brings.
The first is that we are now using type-safe constructs. No more type assertions.
The second is that myCtx
is still completely compatible with context.Context
. This property also comes for free, because we’re just using anonymously embedded context.Context
. Therefore you can use this object as an argument to functions expecting context.Context
without doing anything further. Go’s implicit interface matching FTW.
ctx := &myCtx{}
srv := &http.Server{...}
srv.Shutdown(ctx) // totally legal!
The down side is obviously that you can not create new contexts out of the myctx
value and still have the easy access to the context-specific value associated with it, since it will be surrounded by a black-box context.Context
object.
newctx, cancel := context.WithCancel(myctx)
// newctx is no longer *myCtx
newctx.value // WRONG!
But then again, your code is probably the only thing that relies on this context-specific field anyway. And since you have control over it, it’s up to you to re-create a new context that inherits the value. Maybe you can write code like this:
// Create a new myCtx with associated cancellation
func (ctx *myCtx) WithCancel() (*myCtx, func()) {
newctx, cancel := context.WithCancel(ctx.Context)
return &myCtx{
Context: newctx,
value: octx.value,
}, cancel
}newctx, cancel := myctx.WithCancel()
// newctx can be used as *myCtx
newctx.value // SAFE!
This will allow you to inherit the required context-specific fields while using a quasi-context.Context
compatible API.
There are legitimate uses for context.WithValue
, but in most cases I believe this sort of simple wrapping that matches your particular needs are MUCH safer compared to trying very hard to using vanilla context.Value
Happy hacking!