无缓冲channel导致协程永久泄漏,因其同步语义要求收发goroutine必须同时就绪;若select中超时分支先触发,接收方退出,则发送方goroutine将永久阻塞且无法被gc回收。

无缓冲 chan 一用就 leak,不是代码写得不够“优雅”,而是它天生要求收发双方严格同步——只要有一方缺席或提前退出,另一方就卡死在 chan send 或 chan receive 状态,永远无法被 GC 回收。
为什么 select + 无缓冲 chan 是泄漏高发区
超时分支()只让当前 goroutine 返回,但已启动的后台 goroutine 仍在执行 <code>ch 。由于 <code>ch 是无缓冲的,且主 goroutine 已退出、再无人读,该发送操作将永久阻塞。
- runtime 不报错,
pprof/goroutine?debug=1里能看到大量chan send状态 - 每个泄漏 goroutine 至少占 2KB 栈 +
hchan结构体 + 值对象内存 - 高频超时场景下(如 HTTP client 调用),几秒内就能堆积上百个泄漏协程
带缓冲 chan 并非万能解药
把 make(chan T) 改成 make(chan T, 1) 确实能让发送立即返回,但它只是把“阻塞点”从发送端转移到了接收端逻辑上:如果 select 走了超时分支,ch 里那条数据就没人消费,后续再往同个 ch 发送仍可能阻塞(尤其缓冲区满后)。
- 缓冲大小必须 ≥ 可能并发写入的最大 goroutine 数,否则仍有阻塞风险
- 若多个 goroutine 共享一个缓冲
ch,但只读一次,剩余数据会滞留,造成隐性资源占用 - 缓冲不能解决所有权混乱问题:谁关?谁读完?不明确照样 leak
真正安全的做法是 context + 明确 channel 所有权
让 goroutine 主动响应取消信号,而不是靠通道机制“等对方来救”。发送方应监听 ctx.Done(),并在收到后立即放弃发送。
- 发送方代码必须像这样写:
select { case ch - channel 创建者即关闭者;若只有单一写入方,由它
close(ch);接收方只用for range ch或配合ctx.Done() - 避免在闭包中直接捕获外部
ch,而未传入ctx—— 这是最常见的漏掉监听的写法
最易被忽略的一点:泄漏往往不出现在你写的主逻辑里,而出现在那些“应该只运行一次”的辅助 goroutine 中,比如日志上报、指标采集、连接保活。它们一旦卡在无缓冲 chan 上,就不会再响应任何 shutdown 信号,连服务优雅退出都做不到。











