Go 里 context 的几个误区
review 代码时发现几个同事(包括以前的我)对 context 的理解有偏差,整理一下。
误区一:把 context 当参数容器
最常见的就是 ctx = context.WithValue(ctx, "userID", id),然后在很深的地方 ctx.Value("userID")。
问题有三个:
- 类型不安全,取出来是
interface{},每次都要断言 - key 用字符串会撞(不同包用同一个字符串)
- 调用方看不出这个函数依赖什么
context 只应该传跨切面的东西:请求 ID、trace、超时。业务参数就该老老实实当函数参数。
误区二:key 用 string
如果非要用,key 必须是自定义类型:
type ctxKey int
const (
ctxUserID ctxKey = iota
)
ctx = context.WithValue(ctx, ctxUserID, id)
id, _ := ctx.Value(ctxUserID).(int64)
这样别的包定义同名 key 也不会冲突。
误区三:把 context 存进 struct
type Server struct {
ctx context.Context // 错
}
标准库明确说不要这么干。context 应该沿着调用链显式传,而不是藏在对象里。存进去之后,这个 context 的生命周期就和对象绑定了,超时/取消的语义全乱。
误区四:不调用 cancel
ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel() // 这行不能省
即使超时时间还没到,也必须调 cancel,否则父 context 会一直持有子节点的引用,直到超时——高频调用会泄漏 goroutine 和内存。
go vet 能查出大部分「创建了 cancel 但没调用」的情况,建议加到 CI 里。
一个正面例子
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
ctx, span := tracer.Start(ctx, "GetUser")
defer span.End()
u, err := s.repo.Find(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return u, nil
}
context 只承担「链路控制 + 超时 + trace」,业务参数走形参,错误用 %w 包一层。这样写,谁看都清楚。