为什么需要 context
在 Go 里,一个请求往往会派生出多个 goroutine:查数据库、调下游服务、读缓存……这些任务共享同一个生命周期——上游超时了,下游也应该跟着停。如果只靠 channel 手动广播取消信号,代码会迅速变成一团乱麻。context 就是 Go 官方给出的标准答案:它把「截止时间、取消信号、请求域数据」打包成一个可以沿调用链传递的值。
context 的三种派生方式
所有 context 都从 context.Background() 或 context.TODO() 起步,再按需派生:
WithCancel:手动触发取消,适合「用户断开连接就停」的场景。WithTimeout/WithDeadline:到时间自动取消,适合「调用下游最多等 200ms」。WithValue:挂载请求域数据,谨慎使用。
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // 务必调用,释放定时器资源
实战一:给下游调用加超时
最常见也最容易出错的,是 HTTP 客户端忘了传 context,导致 goroutine 和连接泄漏。正确做法是把 ctx 塞进 http.NewRequestWithContext:
func fetchUser(ctx context.Context, id string) (*User, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.example.com/users/"+id, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err // ctx 超时或取消时,这里会立即返回
}
defer resp.Body.Close()
// ... 解析 resp
}
一旦 ctx 超时,Do 会立刻返回错误,调用方不会卡死。注意:defer resp.Body.Close() 必须放在确认 resp != nil 之后,否则在超时报错时 resp 为 nil 会直接 panic。
实战二:跨 goroutine 的取消传播
父 context 取消后,所有由它派生的子 context 都会收到信号。利用这一点,可以干净地实现「一停全停」:
func crawlAll(ctx context.Context, urls []string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errCh := make(chan error, len(urls))
for _, u := range urls {
go func(u string) {
if err := fetchUser(ctx, u); err != nil {
cancel() // 任意一个失败,取消其余任务
errCh <- err
}
}(u)
}
for range urls {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errCh:
return err
}
}
return nil
}
经验法则:凡是接受 ctx 的函数,都应该把 ctx 作为第一个参数,并且「只向下传递、不向上存储」。
四个常见误用
context 用错比不用更危险,下面四个坑尤其高频:
| 误用 | 后果 | 正确做法 |
|---|---|---|
| 把 ctx 存进 struct 字段 | 生命周期混乱,难以测试 | 作为函数第一个参数显式传入 |
| 用 WithValue 传普通业务参数 | 类型不安全、可读性差 | 用普通参数显式传递 |
| 派生后忘记调用 cancel | timer / goroutine 泄漏 | defer cancel() 紧跟 WithXXX |
| 用 TODO() 混过编译 | 取消信号断链 | 显式传入上游 ctx |
其中「忘记 cancel」最隐蔽。WithTimeout 内部会启一个定时器,不调用 cancel 它就不会被释放,高并发下会缓慢泄漏内存和 goroutine。记住一个固定句式:ctx, cancel := ... 之后立刻 defer cancel()。
小结
context 不是银弹,它管的是「取消与截止」,不该承担普通参数传递的职责。掌握 WithCancel / WithTimeout 的派生关系,坚持 ctx 作为首参、defer cancel、不滥用 WithValue,你的 Go 服务在超时控制和并发取消上就会稳得多。
发表回复