context 用于传递取消信号、超时控制和请求范围值,而非管理并发;goroutine 自行协作取消,select + ctx.Done() 是唯一可靠入口,因 ctx.Done() 关闭后可被 select 确定捕获,避免竞态与阻塞。

Go 的 context 不是用来“管理并发任务”的,而是用来**传递取消信号、超时控制和请求范围值**的;真正执行并发的是 goroutine,context 只负责告诉它们“该停了”。混淆这点会导致误用——比如在非阻塞操作里盲目调用 ctx.Done(),或在不监听 ctx.Done() 的 goroutine 中以为它会自动退出。
为什么 select + ctx.Done() 是唯一可靠取消入口
Go 没有强制中断 goroutine 的机制,取消必须由 goroutine 自己协作完成。唯一被语言保证可触发的信号是 ctx.Done() 返回的 chan struct{},且它只会在取消、超时或截止时间到达时被关闭(因此可被 select 捕获)。
常见错误:
- 直接检查
ctx.Err() != nil而不配合select—— 这是竞态:可能刚判完就取消,但 goroutine 已继续执行下一段 - 在循环里反复调用
time.Sleep却没在每次迭代前 select 等待ctx.Done()—— 一次 sleep 就可能卡死几十秒 - 把
ctx传进一个不读ctx.Done()的第三方函数(如某些阻塞 I/O 库),结果 cancel 完全无效
正确模式始终是:
立即学习“go语言免费学习笔记(深入)”;
select {
case <-ctx.Done():
return ctx.Err() // 或清理后 return
default:
}
// 继续工作
WithCancel / WithTimeout / WithDeadline 的实际差异
三者都返回新 context 和取消函数,但触发条件不同,选错会导致逻辑 bug:
-
context.WithCancel(parent):纯手动取消,适合用户点击“停止”、服务收到 SIGTERM 等场景;必须显式调用返回的cancel() -
context.WithTimeout(parent, 5*time.Second):等价于WithDeadline(parent, time.Now().Add(5*time.Second)),适合 RPC 调用、数据库查询等有明确耗时上限的操作 -
context.WithDeadline(parent, t):基于绝对时间,适合跨多个子任务共享同一截止点(如 HTTP handler 中所有下游调用共用同一个 deadline)
注意:WithTimeout 的计时器在 context 被取消后不会自动 stop,如果大量创建又不 cancel,会泄漏 timer goroutine —— 所以务必在不用时调用 cancel()。
如何安全地向子 goroutine 传递 context
不是所有地方都能直接传 ctx,关键看调用链是否支持:
- 标准库几乎全部支持:如
http.Client.Do(req.WithContext(ctx))、database/sql.Conn.QueryContext(ctx, ...)、time.AfterFunc不支持,得自己包装成select { case - 自定义函数必须显式接收
ctx context.Context参数,并在阻塞点(I/O、channel receive、sleep)插入select判断 - 切忌用
context.Background()或context.TODO()替代传入的ctx—— 这等于绕过整个取消链路
典型反例:
go func() {
// 错!丢失了父 ctx 的取消能力
http.Get("https://example.com")
}()
应改为:
go func(ctx context.Context) {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
http.DefaultClient.Do(req)
}(parentCtx)
Value 与取消无关,但常被误用
context.WithValue 只用于传递请求范围的**不可变元数据**(如用户 ID、trace ID),不是用来传配置、工具实例或状态对象的。它的 key 必须是 unexported 类型,否则极易发生 key 冲突;而且它不参与取消逻辑 —— 即使 context 被 cancel,ctx.Value(key) 仍能取到值。
容易踩的坑:
- 用
string或int当 key —— 多个包都用"user_id"就覆盖了 - 传指针或结构体并试图在 goroutine 里修改它 —— context 值是只读语义,修改不会同步,还可能引发 data race
- 在中间件里反复
WithValue套娃,导致 context 树过深、内存占用上升
真正需要共享状态时,应该用显式参数、闭包变量或外部 sync.Map,而不是塞进 context。
最常被忽略的一点:context 取消是单向广播,没有“恢复”或“重置”机制。一旦 cancel() 被调用,所有下游 ctx.Err() 永远返回非 nil,且 ctx.Done() 永远保持关闭。设计时必须按“一次性生命周期”来考虑,不能指望 reuse 同一个 context 实例做多次请求。










