Context Values

Carry request-scoped data on a context with unexported keys, and keep dependencies out of it.

Syntaxctx := context.WithValue(parent, keyType{}, val); v, ok := ctx.Value(keyType{}).(T)

context.WithValue(parent, key, val) returns a child context in which Value(key) gives back val. Both key and value are of type any, so anything can go in and whatever comes out needs a type assertion.

It exists for data that belongs to one request and has to reach code that never asked for it: a request id for log lines, the authenticated user, a trace identifier. Those cross many function boundaries and adding them as parameters to every one of them would be worse.

The typed-key idiom

Keys are compared by value and by type. A string key is a collision waiting to happen, because another package can write the same string into the same context and overwrite yours. So the key is an unexported type in your own package, usually an empty struct:

type requestIDKey struct{}

No other package can name that type, so no other package can clash with it. Then write two small functions, one to put the value in and one to take it out, and let nothing else touch the key.

Why it is not dependency injection

A database handle, a logger or a config struct is not request data. Those are things your code needs in order to exist. Put them in a struct and give your functions methods on it. A missing struct field fails to compile; a missing context value fails in production, on one code path, at three in the morning.

What trips people up

Value walks up the chain of parents on every lookup, so a context with many values is a short linear search, not a map.

Example

Example · go
package main

import (
	"context"
	"fmt"
)

// requestIDKey is unexported, so no other package can collide with it.
type requestIDKey struct{}

// withRequestID returns a child of ctx carrying id.
func withRequestID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, requestIDKey{}, id)
}

// requestID reports the id stored in ctx, and whether there was one.
func requestID(ctx context.Context) (string, bool) {
	// The two-result assertion is required: Value returns nil when unset.
	id, ok := ctx.Value(requestIDKey{}).(string)
	return id, ok
}

func main() {
	ctx := withRequestID(context.Background(), "req-7")
	id, ok := requestID(ctx)
	fmt.Println(id, ok)

	// A context that never had an id reports the zero string and false.
	id, ok = requestID(context.Background())
	fmt.Printf("%q %v\n", id, ok)
}

// Output:
// req-7 true
// "" false

When to use it

  • Attach a request id at the edge of a service so every log line can be correlated.
  • Make the authenticated user available to code several layers below the handler.
  • Carry a trace or span identifier through code that knows nothing about tracing.
  • Pass a per-request locale or tenant id into shared formatting helpers.

More examples

Set once at the edge, read deep inside

The key type stays private to the auth package and the two exported functions are the only way in or out. Callers never see context.WithValue or a type assertion, so the value cannot be stored under the wrong key or read as the wrong type. This is the standard library's own pattern and it is what makes context values tolerable.

Example · go
// file: auth/context.go
package auth

type userKey struct{}

// WithUser returns a child context carrying the authenticated user.
func WithUser(ctx context.Context, u User) context.Context {
	return context.WithValue(ctx, userKey{}, u)
}

// User reports the authenticated user, and whether the request had one.
func User(ctx context.Context) (User, bool) {
	u, ok := ctx.Value(userKey{}).(User)
	return u, ok
}

// file: billing/refund.go
func Refund(ctx context.Context, orderID int64) error {
	u, ok := auth.User(ctx)
	if !ok {
		return errors.New("refund: no authenticated user in context")
	}
	return record(ctx, orderID, u.ID)
}

The same data as a dependency instead

The first version compiles even when nothing ever put a database in the context, and it uses a string key that any package can overwrite. The second cannot be built without a database. Use this as the test: if the value would be the same for every request, it is a dependency and belongs in a struct field.

Example · go
// Wrong: a database handle is a dependency, not request data.
func loadBad(ctx context.Context, id int64) error {
	db, ok := ctx.Value("db").(*sql.DB)
	if !ok {
		// Nothing caught this at compile time.
		return errors.New("no db in context")
	}
	_, err := db.ExecContext(ctx, "UPDATE orders SET seen = 1 WHERE id = ?", id)
	return err
}

// Right: dependencies live in a struct, ctx carries only cancellation.
type Store struct {
	db *sql.DB
}

func (s *Store) Load(ctx context.Context, id int64) error {
	_, err := s.db.ExecContext(ctx, "UPDATE orders SET seen = 1 WHERE id = ?", id)
	return err
}

Two packages, two keys, no collision

Both packages define an identically named, identically shaped key type, and both can store a value on the same context without either one shadowing the other. Keys are compared by type as well as value, and trace.spanKey and auth.spanKey are different types. The same code with the string key "span" in both packages would silently lose one of the two values.

Example · go
// file: trace/context.go
package trace

// An empty struct type, unexported.
type spanKey struct{}

func WithSpan(ctx context.Context, s Span) context.Context {
	return context.WithValue(ctx, spanKey{}, s)
}

// file: auth/context.go
package auth

// Also an empty struct type, also called the same thing locally.
type spanKey struct{}

func WithSpan(ctx context.Context, s string) context.Context {
	return context.WithValue(ctx, spanKey{}, s)
}

Discussion

  • Be the first to comment on this lesson.