写Go的时候,很多人图省事,在多个goroutine里看到条件满足就直接close(ch),结果程序跑着跑着就崩了:panic: close of closed channel。这问题不常在本地复现,一上生产环境就抽风,排查起来特别挠头。
为什么不能随便关channel?
Go语言规定:channel只能被关闭一次,且只能由发送方关闭。如果多个goroutine都判断‘该关了’然后各自执行close(),必然有至少一个会撞上已关闭状态,直接panic。
比如这个常见场景:后台任务监听HTTP请求,同时用一个done channel通知所有worker退出。如果两个worker几乎同时检测到done信号,又都没加锁,就可能双双调用close(done)——崩。
典型错误写法
func startWorkers(ch chan int, done chan struct{}) {
for i := 0; i < 3; i++ {
go func() {
for {
select {
case x := <-ch:
fmt.Println(x)
case <-done:
close(done) // ❌ 错!多个goroutine都可能走到这里
return
}
}
}()
}
}
靠谱做法:只让一个人关
最简单有效的办法——把关闭逻辑交给一个明确的‘负责人’。通常是启动goroutine的那个函数,或者单独起一个协调goroutine。
改法示例:
func startWorkers(ch chan int, done chan struct{}) {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case x := <-ch:
fmt.Println(x)
case <-done:
return // ✅ 不关,只退出
}
}
}()
}
// 启动后,由主goroutine统一关
go func() {
wg.Wait()
close(done) // ✅ 只有一处关闭
}()
}
额外提醒两件事
1. 别对nil channel调close——同样panic。初始化channel时确保不是nil,或加判空(虽然一般不会漏)。
2. 接收方不用管channel关没关,for-range自动处理;但用val, ok := <-ch方式读取时,ok为false说明已关闭且无数据,这时候再关就是画蛇添足。
线上遇到过一次,监控报警说某服务每小时panic一次,查日志发现总在凌晨3点左右触发,最后定位到是定时清理goroutine的代码里,两个清理协程同时判断超时,争着关同一个done channel。改成单点关闭后,再没出过问题。