Let’s talk about keys for context values.
type Context
type Context interface { … // A key identifies a specific value in a Context. Functions that wish // to store values in Context typically allocate a key in a global // variable then use that key as the argument to context.WithValue and // Context.Value. A key can be any type that supports equality; // packages should define keys as an unexported type to avoid // collisions.
There’s a lot to unpack in this small, seemingly innocuous paragraph. Probably two or three full-length blog posts worth, if we try. But for today, let’s explain what it means by “collisions”.
Let’s imagine your code base has a package for the database, we’ll call it package db
, and one for authentication, package auth
.
Both packages have a sense of a user
, that is request-scoped. In the auth
package, the user represents the user that has logged into the HTTP service. In the db
package, it represents a database role, for data-layer access control. So two different concepts of a user
.
To accomplish this, we have:
package auth
/* ... */
authUser := LookupAuthUser(/* .. */)
ctx = context.WithValue(ctx, "user", authUser)
as well as:
package db
dbUser := LookupDBUser(/* .. */)
ctx = context.WithValue(ctx, "user", dbUser)
Can you see where we’re going with this?
These two packages are likely to interact with each other in certain ways. For example the auth
package may call a function in the db
package to look up the user’s password for validation purposes… And when that happens, the db
package may do something like this:
user, ok := ctx.Value("user").(*DBUser)
if !ok {
return errors.New("db user not found")
}
If the "user"
key exists, but was set by the auth
package, that type assertion will fail due to a context key collision—two different packages attempting to use the same key for different purposes.
Next we’ll be looking at techniques to avoid this problem.