电脑港
白蓝主题五 · 清爽阅读
首页  > 软件应用

Go并发中关闭channel的几个坑,别等panic了才想起来

Go的时候,channel用得飞起,尤其是做生产者-消费者模型、任务分发、超时控制这些场景。但一不小心,关闭一个已经被关掉的channel,或者往已关闭的channel里发数据,程序立马panic:"send on closed channel" 或 "close of closed channel"——这事儿真不稀罕。

谁该关channel?只由发送方关

这是最常被忽略的一条铁律:channel 应该由最后一个发送数据的goroutine来关闭,而不是随便哪个协程想关就关。比如下面这个反例:

ch := make(chan int, 1)

go func() {
    ch <- 1
    close(ch) // ✅ 发送完立刻关,看起来没问题
}()

go func() {
    time.Sleep(10 * time.Millisecond)
    close(ch) // ❌ 另一个goroutine又关了一次!panic!
}()

<-- 程序大概率崩在这里

现实中更隐蔽的情况是:多个worker同时处理一批任务,有人觉得“我干完了,该关channel了”,结果别人还在往里塞数据,或者也准备关——乱套了。

接收方千万别关channel

有些新手图省事,在for-range循环里收到零值或想“清场”时直接close(ch),这是典型误用。for-range本身会自动检测channel是否关闭,不需要手动干预。强行关,轻则panic,重则让其他还在读的goroutine出问题。

ch := make(chan string, 2)
ch <- "hello"
ch <- "world"
close(ch)

// 正确写法:只读,不关
for msg := range ch {
    fmt.Println(msg)
}

// 错误示范(别这么干):
// for msg := range ch {
//     fmt.Println(msg)
//     close(ch) // ❌ 接收方关channel,第一次迭代后就panic
// }

不确定谁在写?那就别关

如果channel生命周期由多个goroutine共同管理(比如动态增删worker),又没法清晰判定“最后一个发送者”,最稳妥的做法是:根本不关channel。改用带超时的select + done channel配合,或者用sync.WaitGroup协调结束时机。

例如,用done信号代替关闭:

done := make(chan struct{})
ch := make(chan int)

go func() {
    defer close(ch)
    for i := 0; i < 5; i++ {
        select {
        case ch <- i:
        case <-done:
            return
        }
    }
}()

// 主goroutine控制何时停止
time.Sleep(time.Second)
close(done) // 不关ch,而是通知发送方退出

for v := range ch {
    fmt.Println(v)
}

怎么安全地判断channel是否已关?

Go没有内置函数能查channel状态,但可以用带ok的接收语法间接感知:

v, ok := <-ch
if !ok {
    // channel已关闭且无剩余数据
    fmt.Println("channel closed")
}

注意:这只能告诉你“现在能不能读到新数据”,不能预测未来会不会被关——所以还是得靠设计规范,而不是靠运行时检查补救。

一句话提醒自己

写代码时默念一遍:谁发数据,谁关channel;没人发了再关;不确定就不关;接收方永远不碰close。多写两行WaitGroup或done channel,远比调试一个随机panic来得轻松。