
为什么 context.WithCancel 不能只调用一次就完事
协程泄露往往不是因为没用 context,而是用了但没在正确时机调用 cancel()。比如在 HTTP handler 里创建子协程去查数据库,ctx 传进去了,但 handler 返回后没人调用 cancel(),子协程就卡在 select 里等永远不来的信号。
常见错误现象:net/http: aborting request body 日志大量出现,pprof 显示 goroutine 数持续上涨,且堆栈停在 runtime.gopark 或 context.wait。
- 必须确保每个
context.WithCancel都有且仅有一次对应的cancel()调用,通常放在 defer 中 - 不要把
cancel函数传给下游协程——它该由创建者控制生命周期 - HTTP handler 中推荐用
req.Context(),而不是自己 new 一个带 cancel 的 context;只有需要主动触发取消时才用WithCancel
子协程里怎么安全监听 ctx.Done() 并清理资源
光读 <ctx>.Done()</ctx> 不够,很多资源(比如打开的文件、连接池里的连接、正在写的 channel)需要显式释放。一旦 ctx.Done() 关闭,后续操作可能 panic 或阻塞。
使用场景:启动后台 worker 协程处理消息队列,或轮询第三方 API。
立即学习“go语言免费学习笔记(深入)”;
- 所有阻塞操作(
time.Sleep、ch 、<code>http.Do)都应配合select+ctx.Done()使用 - 在
case 分支里做清理:关闭 channel、调用 <code>Close()、重置状态变量 - 避免在
ctx.Done()触发后还往已关闭的 channel 发送数据,会 panic;加select判断 channel 是否 closed 更稳妥
select {
case ch <- item:
case <-ctx.Done():
// 清理逻辑,比如 close(ch)
return
}哪些地方不该用 context.WithTimeout 替代 WithCancel
WithTimeout 是 WithCancel 的语法糖,但它把取消时机“锁死”在时间点上。一旦超时触发,你就失去了手动干预的能力——哪怕外部条件已满足,也得等 timeout 到期。
性能影响:高频创建 WithTimeout 会额外启动定时器 goroutine,对延迟敏感服务不友好。
- 需要根据业务逻辑动态决定是否取消时(比如收到 stop signal、配置变更),必须用
WithCancel+ 手动调用 - HTTP 客户端请求级超时用
http.Client.Timeout或context.WithTimeout没问题;但整个服务生命周期管理别用它 - 测试中模拟 cancel 场景时,
WithCancel比WithTimeout(1 * time.Nanosecond)更可靠、更易断言
goroutine 泄露最隐蔽的坑:for range 遍历未关闭的 channel
这是 Context 取消模式失效的高发区。很多人以为只要把 ctx 传进循环体就能自动退出,其实 for range ch 会一直阻塞等待新值,完全无视 ctx。
错误示例:for item := range ch { select { case —— 这个 <code>select 根本不会执行,因为 range 卡在读 ch 上。
- 改用
for { select { case item, ok := - 确保上游 goroutine 在退出前
close(ch),否则 receiver 会永远等下去 - 如果 channel 是无缓冲的,发送方未被取消时,receiver 即便收到
ctx.Done()也可能卡在,需用带 default 的 select 避免
事情说清了就结束。真正难的不是写对那几行 ctx.Done(),是想清楚每个 goroutine 的生命周期边界在哪——谁创建、谁负责关、关的时候要清理什么。这些边界模糊的地方,才是泄露藏身之处。










