我们首先来看一个报错
- should not use built-in type string as key for value; define your own type to avoid collisions (SA1029)go-staticcheck
代码长这样
上面这段程序的波浪线报错不是warnning
级别的,可以说是编辑器的提示,虽说不影响程序的打包运行,但作为强迫症玩家,属实难以接受
这里是golang针对context.Context 类型的使用定义的规范,下面是一些官方话术的解释
- 使用context.Context时,不应该使用内置类型作为 KV 中 key 的类型,而应该使用自定义的类型来避免冲突。
- 当使用 context.Context 类型保存 KV 对时, key 不能使用原生类型,而应该使用派生类型
我们看一些案例
我们在项目中,通常利用 context.Context 作为一个生命周期的上下文传递,贯穿全局,经常来存一些自定义的键值对,保存新的 context 对象
ctx = context.WithValue(ctx, someKey, someValue)
WithValue
方法标准库的定义为
key 和 val 都是any
类型
func WithValue(parent Context, key, val any) Context {
……
}
该方法的注释有这么一句context keys often have concrete type struct{}
就是建议 key 的类型通常为具体的结构体类型
我们的实际使用中,大多会这么样写
ctx = context.WithValue(ctx, "openid", userOpenID)
潜在问题
现在的项目往往涉及多个包的紧密耦合和团队成员间的协作。在一个 ctx 对象的生命周期中,它需要穿越多个逻辑层或包,每个模块都有可能利用 ctx 来存储相关信息。
以用户模块为例,它可能会使用 ctx 来缓存用户的 openid 字段。这种做法本身是合理的。随后,这个 ctx(以及相应的代码逻辑)继续流转,大家默认使用这个 "openid" 键来存储数据。
然而,当一个紧急需求出现,比如需要快速开发一个群聊功能,并且尽可能地复用现有代码以减少开发工作量时,问题就出现了。可能群聊模块在利用用户模块的代码时,无意中也使用了 "openid" 这个键,这次是用来存储群主的 openid。结果,当代码运行时,支援的开发人员发现了一个奇怪的现象:群主的 openid 似乎在不断地变化,仿佛群主的身份在不断轮换。
处理办法
我们以一种常见的思维方式来处理,大家通常会说对ctx
里的 key 里的内容统一规范管理,大家操作ctx
时都遵循一套规则, 这的确是一个很不错的办法
但是我现在对统一规范管理,这6个字特别厌恶,动不动就统一管理的,随着岁月的流失谁还会想着去看文档,干点儿啥都去先看文档约束,麻烦
这种局部的工作细节,分而治之,尽可能避免集中式的管理显然更加适用,又不是什么大的模块
我们先来一个小小的对比优化代码
type chatGroup string
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "openid", "不是群主")
ctx = context.WithValue(ctx, chatGroup("openid"), "群主")
fmt.Println(ctx.Value("openid"))
fmt.Println(ctx.Value(chatGroup("openid")))
}
输出
不是群主
群主
通过chatGroup
,一眼就能看出来是群聊模块的东西
我们再来简写、优化一点
type chatGroup struct{}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "openid", "不是群主")
ctx = context.WithValue(ctx, chatGroup{}, "群主")
fmt.Println(ctx.Value("openid"))
fmt.Println(ctx.Value(chatGroup{}))
}
struct{}
类型(准确来说空结构体是已初始化的值)也可以作为 KV 的 key 类型,当然了,也应该定义为自定义类型。
使用 struct{}
的好处是,这个类型在 Go 中原则上是不占内存空间和 gc 开销的,可以提升性能
进阶例子
封装一个ctx 引入trace ID
的案例
// traceid包 用于在 context 中维护 trace ID
package traceid
import "context"
type traceIDKey struct{}
// WithTraceID 往 context 中存入 trace ID
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, traceIDKey{}, traceID)
}
// TraceID 从 context 中提取 trace ID
func TraceID(ctx context.Context) string {
v := context.Value(ctx, traceIDKey{})
id, _ := v.(string)
return id
}