Lesson 15 / 25

Context Values and the Rules

Conventions that keep contexts predictable.

First parameter, not a struct field

The conventions from the context package docs: pass a context explicitly as the first parameter, conventionally named ctx; do not store contexts inside structs (a struct outlives the request and hides which call the context belongs to); never pass a nil context (use context.TODO() if unsure). context.WithValue attaches request-scoped data that crosses API boundaries, such as a request ID, trace span or authenticated user, not optional function parameters or dependencies like loggers and database handles. Use an unexported key type so that keys from different packages cannot collide, and provide typed helper functions to set and read the value. Contexts are immutable and safe for use by multiple goroutines.

A typed request-ID helper

An unexported key type prevents collisions.

package reqid

import "context"

type ctxKey struct{} // unexported: no other package can build this key

func With(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, ctxKey{}, id)
}

func From(ctx context.Context) (string, bool) {
	id, ok := ctx.Value(ctxKey{}).(string)
	return id, ok
}

// Usage in a handler:
//   ctx := reqid.With(r.Context(), newID())
//   svc.PlaceOrder(ctx, order) // ctx is always the first parameter

If the function needs it to work, make it a parameter

Values hidden in a context are invisible in signatures and unchecked by the compiler. Keep them for cross-cutting metadata that most functions merely pass along.

Quick check: Which use of context.WithValue follows the documented guidance?

  • Carrying a request ID through a call chain
  • Passing the database connection pool
  • Passing an optional retry count to one function
  • Storing the context in a long-lived service struct
Answer

Carrying a request ID through a call chain — Context values are for request-scoped data that crosses API boundaries.