Go

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, and any(ctxKey{}) does not allocate.
  • For several keys, use type key int with const (userKey key = iota; traceKey).

WithValue panics if the key is nil or not comparable (for example a slice or map).

More on Context

All 35 Context questions