Go context取消信号通过关闭Done()通道实现,子goroutine必须用select监听才能即时响应;仅检查ctx.Err()或未在关键路径监听会导致信号被忽略。

取消信号不是“推”出去的,而是父 context 调用 cancel() 后,所有子 context 的 Done() channel 立即被关闭——监听它的 goroutine 在下一次 select 就会退出,不依赖调度、无延迟、不轮询。
为什么子 goroutine 没收到取消信号?
绝大多数情况不是传播失败,而是 goroutine 根本没在监听 ctx.Done()。
- 写了
if ctx.Err() != nil { return }却没放在循环关键路径上 → 只检查一次,错过后续取消 - 用了
select { case 但漏了其他业务 channel,导致 select 永远阻塞在别的 case 上 - goroutine 启动时传的是
context.Background()或老的 context,压根没接入取消链 - HTTP handler 里启了新 goroutine,但没把
r.Context()传进去,而是用了context.Background()
context.WithCancel 嵌套多层会断链吗?
不会断。Go context 的父子链是通过底层 cancelCtx 结构体维护的双向引用,只要子 context 是从父 context 正确派生而来,取消就会穿透全部层级。
- 中间某层自己调过
cancel()→ 它的Done()已关闭,之后派生的子 context 不再响应更上层的 cancel(因为 channel 无法二次关闭) -
context.WithValue包裹WithCancel派生的 context → 不影响取消链,WithValue只是装饰 - 验证是否链路完整:在子 goroutine 里打印
ctx.Err(),父级cancel()后应立刻变为context.Canceled,不是一直为nil
什么时候该调 cancel()?
cancel() 必须由创建 context 的那一方显式调用,且只调一次;调早了下游还没干活就被砍,调晚了资源就泄漏。
立即学习“go语言免费学习笔记(深入)”;
- HTTP handler 中:在写完响应、或明确失败后立即调,别 defer 到函数末尾(可能提前 return)
- 循环中反复创建
WithCancel:每次都要配对调cancel(),否则 timer 和 goroutine 会累积泄漏 - 把
cancel函数传进 goroutine 是危险操作 —— 很容易被误调或重复调;推荐用闭包封装,或由启动方统一控制 - 数据库查询、HTTP 请求等支持 context 的 API,务必传入该 context,它们内部会监听
Done()并提前中止底层 I/O
真正难的从来不是怎么写 WithCancel,而是判断哪些 goroutine 必须接入取消链、在哪插 select、哪些阻塞调用必须换为 context-aware 版本、以及 cancel 后要不要关文件、释放锁、重置状态 —— 这些地方一漏,context 就只是个空壳。


















