Go并发中关闭channel的几个坑,一不小心就panic

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。改成单点关闭后,再没出过问题。