Why should context keys use an unexported custom type instead of a string? Show the idiomatic pattern.
Question 267MediumGo 1.22 to 1.25
Value compares keys with == on the interface, so both the dynamic type and the value must match. If two packages both use the string "user", they overwrite each other's values without any error. An unexported key type makes collisions impossible, because no other package can create a value of that type. staticcheck (SA1029) warns about built-in key types such as string; go vet has no such check.
package auth
type ctxKey struct{} // zero-size, unexported, compares equal to itself
type User struct{ ID, Name string }
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, ctxKey{}, u)
}
func UserFrom(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(ctxKey{}).(*User)
return u, ok
}
Best practices:
- Expose typed accessor functions and never the key itself, so callers can't store the wrong type under your key.
- Use
struct{}keys: they cost no memory, andany(ctxKey{})does not allocate. - For several keys, use
type key intwithconst (userKey key = iota; traceKey).
WithValue panics if the key is nil or not comparable (for example a slice or map).
More on Context
- Q265What is WithCancelCause, and how does ctx.Err() differ from context.Cause(ctx)?
- Q266What does this print? (WithTimeoutCause and its cancel function)
- Q268What does this print? (context value keys)
- Q269How is Value lookup implemented, and what are its performance implications?
- Q270What belongs in context values and what doesn't?
- Q271Why is storing a context.Context in a struct considered an anti-pattern? Are there exceptions?