context.WithValue 看起来像一个随处可用的“全局变量”,但它有两个陷阱:key 类型冲突和线性查找。这篇用实验复现它们,量层数对查找耗时的影响,并核对文档的建议。
都用字符串 "id" 作 key:包 A 取回的是 "来自包 B 的用户ID"(被包 B 覆盖)
各自定义 struct{} 类型作 key:包 A 取回 "来自包 A 的请求ID",包 B 取回 "来自包 B 的用户ID"
自定义类型 type S string 作 key:ctx.Value("id") = <nil>;ctx.Value(S("id")) = "自定义字符串类型"两个包都用字符串 "id",后写入的值把前面的盖住。各自定义一个 struct{} 类型当 key,就互不干扰;连 type S string 也和普通字符串是不同的 key。这就是文档要求不要用内置类型作 key 的原因。
链长 1 层:查最早加入的 key 3 ns/次;查最后加入的 key 3 ns/次 链长 10 层:查最早加入的 key 12 ns/次;查最后加入的 key 3 ns/次 链长 100 层:查最早加入的 key 136 ns/次;查最后加入的 key 3 ns/次 链长 1000 层:查最早加入的 key 1331 ns/次;查最后加入的 key 9 ns/次
value 函数沿着父链一层一层比较 key。查最外层的 key 只需 3 ns,查最里层的:10 层 12 ns、100 层 136 ns、1000 层 1331 ns。链一般不长,但如果每个中间件都往 ctx 里塞东西,又在深处取外层的值,就会感觉到。
WithValue(ctx, struct{} 类型的 key, 字符串值):每次 1 次分配
WithValue(ctx, 指针 key, 字符串值):每次 1 次分配
WithValue(ctx, struct{} key, 整数值 12345):每次 1 次分配
WithValue(ctx, 自定义 int 类型 key = 3, 字符串值):每次 1 次分配
WithValue(ctx, 字符串 key "id", 字符串值):每次 1 次分配
ctx.Value(key) 查找一层:每次 0 次分配每个 WithValue 分配一个节点(1 次分配);查找不分配。这组测量里换 key 的类型(struct{}、指针、自定义 int、字符串)分配次数都是 1,没看出区别。
取回 42(类型 int)
ctx.Value 取回 any,断言成错误类型:interface conversion: interface {} is int, not string断言错类型会 panic(interface conversion: interface {} is int, not string),所以包里最好封装 FromContext 函数用带 ok 的断言。
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables' static type should be a pointer or interface.
context.go 的 value 函数(节选):
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:文档的建议是只放跨进程、跨 API 的请求范围数据(请求 ID、认证信息),不要拿来给函数传可选参数,后者会让依赖变成隐式的,难以测试;key 要自定义类型,常用 struct{}。源码里 value 函数的循环就是前面看到的逐层比较。