An interesting mistake with Go's context
package that I (sort of) made
Today, Dave Cheney did another Go pop quiz on Twitter, where he asked whether the following code printed -6, 0, '<nil>', or paniced:
package mainimport ( "context" "fmt" ) func f(ctx context.Context) { context.WithValue(ctx, "foo", -6) } func main() { ctx := context.TODO() f(ctx) fmt.Println(ctx.Value("foo")) }
I didn't answer this correctly because I focused my attention on the wrong thing.
What I focused on was the use of the "foo"
string as the context
key, partly because of my experience with languages like Python.
To start with, the context
package's documentation says:
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. [...]
A traditional problem in languages like Python is that two strings
may compare the same without actually being the same thing, and
some code really wants you to present it with the exact same thing.
However, the context
package doesn't require that you present it
with the exact same key, just a key where the interface value of
the key will compare the same.
(Because context
compares interface values, both the value and
the type must match; it's not enough for both values to have the
same underlying concrete type, say string, and to compare identical.
This is why defining your own string type is a reliable away around
collisions between packages.)
So after I worked through all of this, I confidently answered that
this code printed -6. The "foo"
string that the value is set
with is not necessarily the same "foo"
string that it's retrieved
with, but that doesn't matter. However, this is not the problem
with the code. The actual problem is that context.WithValue()
returns a new context with the value set, it doesn't change the
context it's called on. Dave Cheney's code is written as if
.WithValue()
mutates the current context, as f()
ignores that
new context that .WithValue()
provides and returns nothing to
main()
. Since the original context
in main()
is what .Value()
is called on, it has no "foo"
key and the result is actually
'<nil>'.
This problem with the code is actually a quite interesting mistake,
because as far as I can tell right now none of the usual Go style
checkers detect it. This code passes 'go vet
', it produces no
complaints from errcheck
because we're not ignoring an error return value, and tools like
golangci-lint only
complain about the use of the built-in type string as the key in
.WithValue()
. Nothing seems to notice that we're ignoring the
critical return value from .WithValue()
, which turns it into more
or less a no-op.
(Now that Dave Cheney has brought this to the surface, I suspect that someone will contribute a check for it to staticcheck, which already detects the 'using a built-in type as a key' issue.)
Comments on this page:
|
|