必须用select而非if,因为ctx.done()返回只读channel,取消前一直阻塞,直接if判断会永久挂起或panic;select可非阻塞监听并及时响应取消信号。

goroutine监听ctx.Done()为什么必须用select而不是if
因为ctx.Done()返回的是一个只读 channel,它在取消前始终阻塞;如果用if 会直接卡死,Go 不允许对未就绪的 channel 做非阻塞接收。只有<code>select能安全等待 channel 关闭,同时兼容其他分支(比如超时、正常完成)。
常见错误是写成这样:
if <p>正确写法必须带<code>select</code>和<code>default</code>或<code>case</code>守卫:</p>
-
select里至少有一个case分支,且不能全是default - 想“非阻塞检查是否已取消”,得用
select { case - 若逻辑本身耗时不确定,应把
ctx.Done()作为独立case,避免阻塞在其它操作上
级联取消失效的三个典型场景
父子 context 看似自动传播,但实际中常因引用丢失或提前释放导致子节点收不到信号。
- 子 context 被赋值给局部变量后未被任何 goroutine 持有——GC 会回收它,
cancel()调用时找不到该子节点,自然无法递归取消 - 手动调用了子节点的
cancel()函数,但没 defer 或提前调用,导致父 cancel 时子节点已处于“已取消”状态,不再响应父级广播 - 用
context.WithValue()派生出的 context 不具备取消能力,它不实现canceler接口,所以不会被父cancel()递归调用
验证是否真级联:在子 goroutine 中打印ctx.Err(),取消后应为context.Canceled;若仍是nil,说明信号没传下来。
context.WithCancel返回的cancel函数为什么必须调用
不调用cancel()会导致两个隐性问题:内存泄漏 + 取消信号残留。
cancelCtx结构体里有个children map[canceler]struct{},只要子 context 还活着,父节点就会强引用它;而这个 map 不会被 GC 自动清理。哪怕子 goroutine 已退出,只要子 context 变量还存在(比如被闭包捕获、存进 map),父节点就一直 hold 着它。
- 漏调
cancel()→ 子 context 不从children中移除 → 父节点无法释放 → 整棵树泄露 - 即使你没显式保存子 context,只要它被某个 goroutine 监听过
Done(),done通道就会被懒加载创建,而该 channel 一旦创建就不会被 GC(channel 是运行时对象) - 最佳实践:凡
WithCancel都配defer cancel(),除非你明确需要延迟取消(如后台常驻任务)
HTTP handler 中传递 context 的坑:别直接用r.Context()派生子 context
HTTP server 内部对r.Context()做了封装,它本身可能已经是withCancel或withTimeout类型。如果你再用它派生新 context(比如WithTimeout(r.Context(), ...)),会形成两层 cancel 控制,容易引发竞态。
典型表现:上游网关已超时关闭连接,但你的WithTimeout还没到时间,goroutine 继续跑;或者反过来,你的 timeout 先触发,但 HTTP server 还在等你写 response。
- 推荐做法:所有下游调用统一使用 handler 入参的
r.Context(),仅在其基础上加WithValue;如需额外超时,改用WithDeadline并确保 deadline 早于r.Context().Deadline() - 检查是否已被取消:在 handler 开头就
if err := r.Context().Err(); err != nil { return },避免后续无谓工作 - 注意:
http.Request的 context 在连接断开时会立即关闭,但某些中间件(如 Gin 的c.Request.Context())可能包装过,行为略有差异
级联取消不是“设了就完事”的开关,它是靠每个 goroutine 主动检查ctx.Done()、每个派生 context 正确挂载到父树、每次cancel()都被执行,三者咬合才能生效。最容易被忽略的是子 context 的生命周期管理——它不像 goroutine 那样显式启动/退出,而是静默地挂在树上,一不留神就成了内存里的幽灵节点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











