陈默的笔记

Go 里 context 的几个误区

· Go

review 代码时发现几个同事(包括以前的我)对 context 的理解有偏差,整理一下。

误区一:把 context 当参数容器

最常见的就是 ctx = context.WithValue(ctx, "userID", id),然后在很深的地方 ctx.Value("userID")。

问题有三个:

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 包一层。这样写,谁看都清楚。