无缓冲 channel 一用就 leak 是因为其同步语义要求收发双方必须同时就绪,若一方缺席(如只发不收或只收不发),另一方将永久阻塞在 send/receive 操作上,goroutine 无法退出,gc 不回收——这不是内存泄漏,而是执行流被钉死在阻塞点。

无缓冲 chan 一旦收发不配对,协程就卡死不退,GC 完全不管——这不是内存泄漏,是执行流被钉在了阻塞点上。
为什么 make(chan int) 一用就 leak
无缓冲 channel 的语义是同步等待:发送方必须等到接收方就绪才能返回,反之亦然。只要其中一方缺席或提前退出,另一方就永远停在 或 <code>ch 上。
- 主 goroutine 启动子 goroutine 往
ch发数据,自己却没读 —— 子 goroutine 卡在发送上 - 子 goroutine 在
for range ch里等数据,但主 goroutine 已 return、没人再写 —— 子 goroutine 卡在接收上 - 多个 goroutine 往同一个
ch发送,但某个分支提前return,剩下几个永远等不到接收者
这些情况 runtime 不报错,runtime.NumGoroutine() 会持续上涨,pprof 堆栈里全是 chan send 或 chan receive 状态。
用 context.Context 主动控制退出时机
不能依赖“对方会不会来”,得让 goroutine 自己能感知到该停了。最稳妥的方式是把 ctx.Done() 接入阻塞点,尤其是 select 中。
- 发送前加
select分支监听ctx.Done(),超时或取消时直接 return - 接收循环不要写
for range ch,改用for { select { case v := - 传入的
ctx必须由调用方负责 cancel,比如用context.WithTimeout或context.WithCancel
示例:
go func(ctx context.Context, ch chanctx, ch)
关闭通道前必须明确所有权
谁创建、谁关;谁发、谁关;谁读、谁不关。混乱的关闭权是泄露温床。
- 如果 sender 是唯一写入方,且知道所有数据已发完,它应调用
close(ch) - receiver 必须用
v, ok := 检查 <code>ok,ok == false说明通道已关,应退出循环 - 绝对不要在 receiver 里 close
ch,也不要在多个 goroutine 里竞争 close - 若 sender 可能 panic 或中途退出,需用
defer close(ch)保底(但仅限单写入场景)
避免在 select 超时分支里漏掉 goroutine 清理
select 的超时只影响当前 goroutine 的流程,不会自动 kill 其他已启动的 goroutine,也不会让 channel 发送“作废”。
- 超时后,如果之前已启动一个 goroutine 往
ch发数据,它仍会卡在ch 上 - 正确做法是:超时分支触发后,显式通知该 goroutine 退出(例如通过另一个
donechannel) - 或者改用带
ctx的发送逻辑,让子 goroutine 自己监听ctx.Done()
最容易被忽略的一点:无缓冲 channel 的泄露不是“有没有数据”的问题,而是“有没有人配对操作”的问题。哪怕只漏掉一次接收,就足以钉住一个 goroutine —— 它不会自己醒来,也不会被 GC 扫掉,只会一直占着栈和调度器资源,直到进程结束。











