{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/avoid-context-key-collisions/",
"description": "Master Go context keys with custom types, avoid collisions using empty structs, and learn accessor patterns for safe request-scoped values.",
"path": "/go/avoid-context-key-collisions/",
"publishedAt": "2025-10-22T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"API",
"Performance"
],
"textContent": "Along with propagating deadlines and cancellation signals, Go's context package can also\ncarry request-scoped values across API boundaries and processes.\n\nThere are only two public API constructs associated with context values:\n\nWithValue can take any comparable value as both the key and the value. The key defines how\nthe stored value is identified, and the value can be any data you want to pass through the\ncall chain.\n\nValue, on the other hand, also returns any, which means the compiler cannot infer the\nconcrete type at compile time. To use the returned data safely, you must perform a type\nassertion.\n\nA naive workflow to store and retrieve values in a context looks like this:\n\nWithValue returns a new context that wraps the parent. Value walks up the chain of\ncontexts and returns the first matching key it finds. Since the return type is any, a type\nassertion is required to recover the original type. Without the ok check, a mismatch would\ncause a panic.\n\nThe issue with this setup is that it risks collision. If another package sets a value\nagainst the same key, one overwrites the other:\n\nThe first value becomes inaccessible because WithValue returns a new derived context that\nshadows parent values with the same key. The original value still exists in the parent\ncontext but is unreachable through the reassigned variable.\n\nTo understand why this collision occurs, you need to know how Go compares interface values.\nWhen you assign a value to an interface{} (or any), Go boxes that value into an internal\nrepresentation made up of two [machine words]: one points to the type information, and the\nother points to the underlying data.\n\nFor example:\n\nEach boxed interface here stores two things: a pointer to the type string and a pointer to\nthe data \"key\". Since both type and data pointers match, the comparison returns true.\n\nWithValue stores both the key and the value as any. When you later call Value, Go\ncompares the boxed key you pass in with those stored in the context chain. If two different\npackages use the same built-in key type and data, like both passing \"key\" as a string,\ntheir boxed representations look identical. Go sees them as equal, and the most recent value\nshadows the earlier one.\n\nIf you want to learn more about how interfaces are represented and compared, [Russ Cox's\npost on Go interface internals] explains it in detail with pretty pictures.\n\nThe fix is to make sure the keys have unique types so their boxed representations differ. If\nyou define a custom type, the type pointer changes even if the data looks the same. For\nexample:\n\nEven though the underlying value is \"key\", the two interfaces now hold different type\ninformation, so Go considers them unequal. That difference in type identity is what prevents\ncollisions.\n\nThe [context documentation] gives this advice:\n\n> The provided key must be comparable and should not be of type string or any other built-in\n> type to avoid collisions between packages using context. Users of WithValue should define\n> their own types for keys. To avoid allocating when assigning to an interface{}, context\n> keys often have concrete type struct{}. Alternatively, exported context key variables'\n> static type should be a pointer or interface.\n\nIn short:\n\n- Keys must be comparable (string, int, struct, pointer, etc.)\n- Define unique key types per package to avoid collisions\n- Use struct{} keys to avoid allocation when stored as any\n- Exported key variables should have pointer or interface types\n\nHere's how defining a unique key type prevents collisions:\n\nEven if another package uses the string \"id\", the key types differ, so they cannot\ncollide.\n\nTo avoid allocation when WithValue assigns the inbound value to interface any, you can\ndefine an empty struct key. Unlike strings or integers, which allocate when boxed into an\ninterface, a zero-sized struct occupies no memory and needs no allocation:\n\nEmpty structs are ideal for local, unexported keys. They are unique by type and add no\noverhead.\n\nAlternatively, exported keys can use pointers, which also avoid allocation and guarantee\nuniqueness. When a pointer is boxed into an interface, no data copy occurs because the\ninterface just holds the pointer reference. Pointers are also ideal for keys that need to be\nshared across packages.\n\nHere, UserIDKey points to a unique struct instance, so equality checks work by pointer\nidentity. The name field exists only for debugging. This avoids allocation and ensures\nexported keys remain unique even when shared between packages.\n\nWhen exposing context values across APIs, you can approach it in two ways depending on how\nmuch control and safety you want to give your users.\n\n1. Expose keys directly\n\nYou can export the key itself and let users interact with it freely:\n\nWhen you export the key directly the caller gains direct access, but they also must:\n\n- do the type assertion themselves and handle the ok result to avoid panics\n- ensure they don't accidentally overwrite values using the wrong key\n\nThe [net/http] package uses this approach for some of its exported context keys:\n\nEach variable points to a distinct struct, making them unique by pointer identity.\n\nThe [serve_test.go] file uses these keys like this:\n\nThe server value is stored in the context and later retrieved using the same pointer key.\nThe user must perform a type assertion and handle it safely.\n\n2. Expose accessor functions\n\nThe other approach is to hide the key and provide accessor functions to set and retrieve\nvalues. This removes the need for users to remember the right key type or perform type\nassertions manually.\n\nThis approach centralizes how values are stored and retrieved from the context. It ensures\nthe correct key and type are always used, preventing collisions and runtime panics. It also\nkeeps the calling code shorter since your API users won't need to repeat type assertions\neverywhere.\n\nWithX / XFromContext accessors appear throughout the Go standard library:\n\n- [net/http/httptrace]\n\n \n\n- [runtime/pprof]\n\n \n\nYou can find similar examples outside of the stdlib. For instance, the [OpenTelemetry Go\nSDK] follows the same model:\n\nThis technique standardizes how values are passed across APIs, eliminates redundant type\nassertions, and prevents key misuse across packages.\n\nClosing words\n\nI usually use a pointer to a struct as a key and [expose accessor functions] when building\nuser-facing APIs. Otherwise, in services, I often define empty struct keys and expose them\npublicly to avoid the ceremony around accessor functions.\n\n\n\n\n[machine words]:\n https://unicminds.com/what-is-a-machine-word-and-its-implications/\n\n[russ cox's post on go interface internals]:\n https://research.swtch.com/interfaces\n\n[context documentation]:\n https://pkg.go.dev/context#WithValue\n\n[net/http]:\n https://cs.opensource.google/go/go/+/refs/tags/go1.25.3:src/net/http/server.go;l=239-251\n\n[serve_test.go]:\n https://cs.opensource.google/go/go/+/refs/tags/go1.25.3:src/net/http/serve_test.go;l=5132-5144\n\n[net/http/httptrace]:\n https://github.com/golang/go/blob/39ed968832ad8923a4bd1fb6bc3d9090ddd98401/src/net/http/httptrace/trace.go#L20-L68\n\n[runtime/pprof]:\n https://github.com/golang/go/blob/39ed968832ad8923a4bd1fb6bc3d9090ddd98401/src/runtime/pprof/label.go#L60-L63\n\n[opentelemetry go sdk]:\n https://github.com/open-telemetry/opentelemetry-go/blob/f0c24571557de839332e48790714a5899c4fd2c6/trace/context.go\n\n[expose accessor functions]:\n #2-expose-accessor-functions",
"title": "Avoiding collisions in Go context keys"
}