写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来得轻松。