Bossy Lobster

A blog by Danny Hermes; musing on tech, mathematics, etc.

Edit on GitHub

Wrapping Behavior of context.WithValue()


Throughout the Go monorepo we use context.WithValue() to "stash" a global value on a root context. For example

ctx = logger.WithLogger(ctx, log)
// ... later ...
log := logger.GetLogger(ctx)

The implementations for stashing a logger.Log are in the same general form as most context wrapping helpers:

type loggerKey struct{}

func WithLogger(ctx context.Context, log Log) context.Context {
    return context.WithValue(ctx, loggerKey{}, log)

func GetLogger(ctx context.Context) Log {
    if value := ctx.Value(loggerKey{}); value != nil {
        if typed, ok := value.(Log); ok {
            return typed
    return nil

Our goal here is to understand how context.WithValue() keeps the data around and how ctx.Value() is able to extract it back out.

Dealing with Unexported Types

In addition, to defining the context.Context interface, the Go standard library also defines some unexported concrete implementations. In particular, emptyCtx is defined to support context.Background() and valueCtx is defined to support context.WithValue().

In order to see inside values of each of these types, we can create a sufficiently similar type and then use the reflect and unsafe packages to "cast" the memory from the standard library types into our own types. For example:

func CtxPointer(ctx context.Context) *int {
    rc := reflect.ValueOf(ctx)
    p := unsafe.Pointer(rc.Pointer())
    return (*int)(p)

func ToEmptyCtx(ctx context.Context) *EmptyCtx {
    p := unsafe.Pointer(CtxPointer(ctx))
    return (*EmptyCtx)(p)

The fields in the types themselves are straightforward to copy over:

type EmptyCtx int

type ValueCtx struct {
    wrapped  context.Context // Intentionally avoid struct-embedding
    key, val interface{}

Unwrapping It All

In order to better understand how context wrapping via context.WithValue() works, we'll stash multiple values for the same key onto a context:

type simpleKey struct{}

func main() {
    ctx1 := context.Background()
    fmt.Printf("ctx1 = %s\n", ToEmptyCtx(ctx1))
    fmt.Printf("  ctx1.Value(simpleKey{}) = %v\n", ctx1.Value(simpleKey{}))
    // Wrap once.
    ctx2 := context.WithValue(ctx1, simpleKey{}, "x")
    fmt.Printf("ctx2 = %s\n", ToValueCtx(ctx2))
    fmt.Printf("  ctx2.Value(simpleKey{}) = %q\n", ctx2.Value(simpleKey{}))
    // Wrap twice.
    ctx3 := context.WithValue(ctx2, simpleKey{}, "y")
    fmt.Printf("ctx3 = %s\n", ToValueCtx(ctx3))
    fmt.Printf("  ctx3.Value(simpleKey{}) = %q\n", ctx3.Value(simpleKey{}))
    // Wrap thrice.
    ctx4 := context.WithValue(ctx3, simpleKey{}, "z")
    fmt.Printf("ctx4 = %s\n", ToValueCtx(ctx4))
    fmt.Printf("  ctx4.Value(simpleKey{}) = %q\n", ctx4.Value(simpleKey{}))

Running the script we see that the latest value stashed for the key wins in valueCtx.Value(). We also see that the second stage (ctx2) wraps the first (0xc00009e000), the third stage (ctx3) wraps the second (0xc0000981b0) and so on.

$ go run docs/go-context-withvalue/main.go
ctx1 = EmptyCtx(0) [address=0xc00009e000]
  ctx1.Value(simpleKey{}) = <nil>
ctx2 = ValueCtx(wrapped=0xc00009e000, key=main.simpleKey{}, val="x") [address=0xc0000981b0]
  ctx2.Value(simpleKey{}) = "x"
ctx3 = ValueCtx(wrapped=0xc0000981b0, key=main.simpleKey{}, val="y") [address=0xc0000981e0]
  ctx3.Value(simpleKey{}) = "y"
ctx4 = ValueCtx(wrapped=0xc0000981e0, key=main.simpleKey{}, val="z") [address=0xc000098210]
  ctx4.Value(simpleKey{}) = "z"