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:

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

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.

← All articles