for range 读取未关闭通道必然死锁,因range只响应close(ch)事件;多写入方须由单一goroutine关通道;done channel适用于纯信号广播,context适用于IO超时等场景;协程退出不释放资源,需defer确保Close。

for range 读取未关闭通道必然死锁
只要用 for v := range ch,通道 ch 就必须被关闭,否则循环永远不会退出。这不是“可能卡住”,而是 Go 运行时强制检测的死锁:所有 goroutine 休眠后直接 panic 报 fatal error: all goroutines are asleep - deadlock!。
常见误解是“发送端退出了,range 就该停”。错——range 不感知 goroutine 生存状态,只响应 close(ch) 事件。哪怕 10 个生产者全跑完了,只要没人关通道,消费者就永远等下去。
- 无缓冲通道:第一个
range接收就阻塞,若无写入方,立刻死锁 - 有缓冲通道:能读完缓冲区内容,但之后仍会阻塞等待更多数据或关闭信号
- 即使加了
time.After或其他超时逻辑,只要没关通道,range本身不退出,超时分支也进不去
多个写入方共用通道时,关闭权必须唯一
10 个 goroutine 同时往一个 chan int 写,又都各自调 close(ch)?运行时立刻 panic:panic: close of closed channel 或更隐蔽的 panic: send on closed channel。
根本矛盾在于:关闭动作不是“通知”,而是“终结”——它让通道进入不可逆的只读终态。多个协程竞争关闭,等于多人同时按电梯关门键,结果不是门关不上,就是门夹断手指。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 关闭责任必须明确归属:通常是协调者(如主 goroutine)或专设的关闭协程
- 写入方只负责发数据,不碰
close;读取方绝对不能关通道(职责越界) - 用
sync.WaitGroup跟踪所有写入 goroutine 完成,再由单一 goroutine 执行close(ch) - 若用
context.WithCancel,就别手动关 channel——让接收方监听ctx.Done()主动退出
done channel 和 context 的适用边界
done chan struct{} 适合纯信号广播:通知“现在退出”,不带超时、不传播取消链。而 context.Context 是为 IO、网络、定时等不确定耗时操作设计的,自带超时、截止时间、嵌套取消能力。
二者混用容易出问题。比如在 select 中同时监听 done 和 ctx.Done(),却没处理 ctx 被取消后 done 仍可能被误读的竞态。
- 仅需“立即停止”的场景(如配置热重载):用
done+close(done)最轻量 - 涉及 HTTP 请求、数据库查询、定时任务:必须用
context,且把ctx传给所有下游调用 - 不要用
done替代context做超时控制——time.After在select中无法取消,易泄漏 -
context.WithTimeout创建的ctx必须配defer cancel(),否则上下文泄漏会拖垮整个服务
协程退出 ≠ 资源释放
goroutine 返回后,它持有的通道引用、文件句柄、数据库连接不会自动消失。GC 只回收无引用对象,而一个未关闭的通道若被闭包捕获、或被全局 map 引用,就会一直留在内存里。
更危险的是“伪退出”:协程因 panic 提前结束,defer 没执行,导致 file.Close()、rows.Close() 被跳过。线上常表现为句柄数缓慢上涨,几天后服务拒绝新连接。
- 所有打开资源必须配
defer xxx.Close(),且确保 defer 在 panic 时仍有效(避免在 defer 前 panic) - 通道若被多个 goroutine 共享,关闭后要确认无 goroutine 仍在尝试读/写——尤其是带缓冲的通道,关闭后还能读完剩余值,但不能再写
- 用
pprof定期检查 goroutine 数量和堆内存中 channel 实例数,比等报警更早发现问题

















