调用 cancel() 本质是关闭 ctx.Done() 通道,使监听 goroutine 立即感知;未调用则子 context 不自动退出(除非 WithTimeout/WithDeadline);ctx.Done() 关闭后 ctx.Err() 才返回非 nil 值,故应优先 select 监听 Done 而非直接判 Err。

context 传递取消信号,本质不是“发消息”,而是“关通道”:调用 cancel() 就立刻关闭 ctx.Done(),所有监听它的 goroutine 都能立即感知。不调 cancel(),哪怕父 context 已结束,子 context 也不会自动退出——除非它本身是 WithTimeout 或 WithDeadline 这类自带终止逻辑的。
为什么 ctx.Done() 关了,ctx.Err() 才有意义
Done channel 关闭后,ctx.Err() 才返回非 nil 值(比如 context.Canceled 或 context.DeadlineExceeded)。直接 if ctx.Err() != nil 判断是错的——channel 没关时 Err 可能还是 nil,但你已经该停了;channel 关了之后 Err 才稳定可读。
- 永远用
select { case 等待信号,而不是轮询ctx.Err() - 在
case 分支里,再调ctx.Err()区分是人为取消、超时,还是其他原因 - 如果操作本身支持 context(如
http.NewRequestWithContext、time.Sleep的替代方案),优先交给它处理,别自己手动 select
WithCancel 创建的 cancel 函数必须显式调用且只调一次
context.WithCancel 返回的 cancel 是个函数,不是开关。它不调,下游就收不到信号;调两次会 panic(Go runtime 报 double cancel)。
- 常见错误:忘记
defer cancel(),导致 goroutine 泄漏或数据库连接长期占用 - 更隐蔽的问题:提前
return导致defer cancel()过早触发,下游还没做完就被砍掉——这时应把cancel()放到业务逻辑真正完成/失败后手动调 - 命名建议保持为
cancel,避免团队协作中遗漏调用
多个 goroutine 共享同一个 ctx,一个 cancel 全部退出是设计目标
这不是 bug,是 context 的核心契约:所有派生自同一 parent 的子 context,共享同一个取消源。上游一 cancel,下游全部感知。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 错误做法:每个 goroutine 自己
WithCancel(ctx)再传进去——这等于各自独立生命周期,取消信号无法广播 - 正确做法:只在源头创建一次
ctx, cancel := context.WithCancel(parent),然后把同一个ctx传给所有要控制的 goroutine - 注意:不要把
ctx当参数塞进循环体里反复重用;每次启动新任务,都应明确决定是否复用当前 ctx,还是新建带 deadline 的子 ctx
超时选 WithTimeout 还是 WithDeadline?看时间语义是否容错
WithTimeout(ctx, 5*time.Second) 是“从现在起 5 秒后取消”,WithDeadline(ctx, time.Now().Add(5*time.Second)) 是“在某个绝对时间点取消”。两者底层都转成 deadline,但精度和行为不同。
- HTTP client 请求通常用
WithTimeout,简单直观 - 与外部系统约定截止时间(比如支付接口要求 15:00:00 前返回),必须用
WithDeadline,否则 NTP 校时可能导致提前或延后取消 - 高负载下频繁调用
time.Now()+Add()有微小漂移,WithDeadline更可控
最难的从来不是写 ctx, cancel := context.WithCancel(...),而是想清楚:哪个 goroutine 负责调 cancel()、它依据什么条件触发、哪些资源需要在 case 里手动释放——这些地方漏掉一个,context 就只剩个空壳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










