无缓冲channel一用就leak是因为其同步语义要求收发双方goroutine必须同时就绪,若任一方缺失或提前退出,另一方将永久阻塞且不被GC回收,导致协程泄漏。
无缓冲 chan 是协程泄露最常见、最隐蔽的源头之一:只要发送或接收一方永远不出现,另一方就卡死不动,且 gc 完全不管——它不是内存对象泄漏,而是执行流卡住不退。
为什么无缓冲 channel 一用就 leak?
无缓冲 channel 的语义是“同步通信”:ch 必须等到有 goroutine 在另一端执行 <code> 才能返回;反之亦然。没有配对操作,就永久阻塞。
- 主 goroutine 启动子 goroutine 往
ch发数据,自己却没读 —— 子 goroutine 卡在发送上 - 子 goroutine 从
ch读数据,但主 goroutine 已退出、没人再往里写 —— 子 goroutine 卡在接收上 - 多个 goroutine 往同一个无缓冲
ch发送,但错误地提前 return,导致部分 goroutine 永远等不到接收者
这些场景下,runtime 不会报错,runtime.NumGoroutine() 会持续高于预期,pprof 查看堆栈能看到大量 chan send 或 chan receive 状态。
哪些典型写法会当场触发 leak?
以下代码片段都极容易出现在真实项目中:
go func() { ch —— 没人读 <code>ch,这个 goroutine 就再也出不来-
for range ch { ... }但没人 close(ch),且没有其他退出条件,循环永不终止 select { case ch —— 超时分支返回后,<code>ch 可能还在后台 goroutine 中阻塞等待接收者
特别注意:select 的超时分支不会自动取消已启动的 goroutine,也不会让 channel 发送“失效”。它只影响当前 goroutine 的控制流。
怎么修才真正安全?
修复核心是打破“单向依赖”,让发送/接收双方有明确的退出契约:
- 用带缓冲的 channel(如
make(chan error, len(addrs)))—— 所有发送可立即完成,不依赖接收时机,适合结果收集类场景 - 加
context.Context,在 goroutine 内部用select监听ctx.Done(),收到 cancel 信号就主动退出 - 显式关闭 channel 并配合
ok判断:v, ok := ,<code>!ok表示 channel 已关闭,可 break 循环 - 避免让多个 goroutine 共享一个无缓冲 channel 做“结果上报”,改用每个 goroutine 自带的 done channel +
sync.WaitGroup
缓冲大小不能拍脑袋定:设为 1 通常够用,但如果可能并发写入 N 个值,缓冲至少为 N,否则第 N+1 次发送仍会阻塞。
最容易被忽略的细节
很多人以为“只要没 panic,goroutine 就算运行完了”。其实不然:一个卡在 上的 goroutine,栈还在、局部变量还在、甚至持有的 <code>net.Conn 或 *os.File 都没关——它只是不占 CPU,但资源全锁着。这种 leak 在压测后不回落、上线几天后 OOM,才是最要命的。


















