关闭 channel 的唯一安全主体是发送方:只有发送方才能调用 close(),接收方调用会导致运行时 panic;多 goroutine 发送时需协调唯一关闭者;工厂函数返回的 channel 应由内部 sender goroutine 负责关闭。
Go 语言规定:只有负责向 channel 发送数据的一方,才应(且仅能由它)调用 close()。接收方调用 close() 是编译期不报错但运行时 panic 的未定义行为,错误信息为 panic: close of closed channel 或更常见的是 panic: close of nil channel(若 channel 本身为 nil)。
根本原因在于:关闭语义是“我不会再发了”,而非“我不收了”。接收方无权替发送方做此承诺。
close()
select + default 非阻塞发送时,别误把发送失败当成“该关了”,发送失败不等于发送方结束对已关闭的 channel 执行 ch 会立即触发运行时 panic:panic: send on closed channel。这和接收不同——接收已关闭 channel 是安全的(返回零值 + false),但发送不是。
典型踩坑场景:
sync.WaitGroup 等待所有 sender 结束,但在 Wait() 返回后才调用 close() —— 这看似安全,但如果某个 sender 在 WaitGroup.Done() 和 close() 之间抢到了调度并再次发送,就 panic正确做法是:确保所有发送操作彻底完成(包括最后一个 ch 返回)之后,再调用 close(ch)。常用模式是让 sender 自己关:
go func() {
defer close(ch)
for _, v := range data {
ch <- v
}
}()从已关闭的 channel 接收不会 panic,但会持续返回零值。是否已关闭,只能靠接收时的第二个返回值 ok 判断:
v, ok := <-ch
if !ok {
// channel 已关闭,且无剩余数据
}忽略 ok 直接使用 v,会导致逻辑错误(比如把 int 类型的零值 0 当成有效数据处理)。
ok 检查,所以 for v := range ch { ... } 在 channel 关闭后自动退出,这是最推荐的接收模式select 多路接收时,每个 分支都必须配 ok 判断,不能依赖 default 或超时分支来规避
len(ch) == 0 && cap(ch) == 0 判断是否关闭——len/cap 对 channel 无意义,且无法反映关闭状态声明但未初始化的 channel 变量值为 nil。对 nil channel 执行发送或接收操作,当前 goroutine 会**永久阻塞**(不是 panic),且无法被其他 goroutine 唤醒——这是 Go 调度器的明确设计。
常见误用:
var ch chan int,未 make 就传入函数或启动 goroutine 使用if cond { ch = make(...) },但漏掉 else 分支,导致 ch 保持 nilnil 模拟“禁用”channel,却忘了它会卡死整个 goroutine调试提示:若程序卡在某处且 goroutine 状态为 chan receive 或 ch,先检查对应 channel 是否为 
nil。
channel 关闭本身不复杂,难的是在多 goroutine 协作中准确界定谁拥有生命周期控制权。一旦发送方不确定、关闭时机不唯一、或接收方擅自干预,问题就会从编译期消失,转为难以复现的运行时 hang 或 panic。