否,go的context取消不是“链表广播”,而是递归遍历children map逐层调用子节点cancel(),done通道关闭是逐级触发而非父节点直接关闭所有子孙通道。

Go 的 Context 取消不是“层层通知”,而是“链表广播”——父节点 cancel() 会直接关闭所有子孙 Done channel,不依赖逐层唤醒。
context.CancelFunc 调用后到底发生了什么
它触发两个原子动作:Done channel 立即关闭 + 父 context 从 children map 中移除对该子 context 的引用。缺一不可。
- 只关闭 channel 不清理引用 → goroutine 不泄漏,但 timer 和子 context 内存仍被父节点强持有,GC 无法回收
- 只清理引用不关闭 channel → 子 context.Done() 永远阻塞,监听它的 goroutine 卡死
- cancel() 是幂等的:重复调用不会 panic(
context.cancelCtx实现里有if c.err != nil保护),但漏调必泄漏
WithTimeout 创建的 timer 为什么泄漏
context.WithTimeout 内部启动一个 *time.Timer,并 spawn 一个 goroutine 等待触发。这个 timer 不会因业务函数 return 自动 Stop。
- 现象:pprof 显示大量
runtime.timer实例,或 goroutine 堆栈卡在timerproc - 根因:没调
cancel()→ timer 未Stop()→ goroutine 持续运行 → 每次请求新增一个 timer - 验证方式:在
WithTimeout后加fmt.Printf("timer addr: %p\n", &t)(需反射访问),对比 pprof 中地址是否持续增长
HTTP handler 中 defer cancel() 为什么危险
net/http 的 r.Context() 已由服务器自动管理(客户端断连、超时等都会触发取消)。你在中间件里套一层 WithTimeout 再 defer cancel(),等于提前终止了下游可用的上下文。
- 错误写法:
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second); defer cancel()→ handler 拿到的 ctx 可能在读 body 前就已关闭 - 正确场景:仅当你创建**独立于请求生命周期的子 context**(如异步发消息、后台落库)才需要手动 cancel
- 若必须设超时,应通过
http.Transport分层控制:用DialContext控制建连、TLSHandshakeTimeout控制握手,而非只靠顶层 context
父子 context 的超时时间不能叠加
子 context 的 deadline 总是取父 context 剩余时间和自身 timeout 的较小值。这不是 bug,是级联取消的底层约束。
- 示例:
parent剩余 2s,child := context.WithTimeout(parent, 10*time.Second)→child实际最多活 2s - 后果:上游网关设了 5s 超时,你却在 handler 里给 DB 查询设 10s,结果 DB 还没开始连就被父 context 关闭了
- 调试技巧:打印
ctx.Deadline(),确认实际生效的截止时间,别只信传入的 timeout 值
最容易被忽略的是 cancel() 的调用时机 —— 它必须出现在所有可能的返回路径上,而不仅靠 defer;更隐蔽的是,很多 SDK(比如 database/sql)只在 query 执行阶段响应 Done channel,建连阶段仍可能绕过 context 控制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











