go / ctxkey
I use type-safe context keys to avoid type assertions
when storing and retrieving values from context.Context.
The problem
Go's context.Value() returns any, requiring type assertions:
type ctxKey struct{}
var userKey = ctxKey{}
ctx = context.WithValue(ctx, userKey, user)
// Every retrieval needs a type assertion
v := ctx.Value(userKey)
if v == nil {
return nil, false
}
user := v.(*User) // easy to get wrong
This pattern is repetitive, error-prone, and doesn't support default values.
The pattern
package ctxkey
import "context"
type Key[V any] struct {
name *string
defVal *V
}
func New[V any](name string, defaultValue V) Key[V] {
return Key[V]{name: &name, defVal: &defaultValue}
}
func (k Key[V]) WithValue(ctx context.Context, val V) context.Context {
return context.WithValue(ctx, k.name, val)
}
func (k Key[V]) Value(ctx context.Context) V {
if v, ok := ctx.Value(k.name).(V); ok {
return v
}
if k.defVal != nil {
return *k.defVal
}
var zero V
return zero
}
Key points:
- Pointer as key:
k.nameis a*string. EachNew()call allocates a new pointer, making keys unique even with the same name string. - Generic value type: No type assertions at call sites.
- Default values: Unpopulated keys return the default, not zero.
Usage
Define keys at package level:
package auth
var UserKey = ctxkey.New("auth.User", (*User)(nil))
var TimeoutKey = ctxkey.New("server.Timeout", 30*time.Second)
Store and retrieve without type assertions:
// Middleware stores the user
ctx = auth.UserKey.WithValue(ctx, user)
// Handler retrieves it - returns *User, not any
user := auth.UserKey.Value(ctx)
// Timeout has a default if not set
timeout := TimeoutKey.Value(ctx) // 30s if unset
When to use
- Context values accessed in multiple places
- Middleware chains passing request-scoped data
- When you want sensible defaults for missing values
- Anywhere you'd write
ctx.Value(key).(*Type)
See reqid for a simpler pattern when you don't need default values or multiple keys.
The naming convention "package.KeyName" helps with debugging
since context values are often printed in logs and stack traces.