“panic: send on closed channel” 的修法常被写成“加个 recover”。这篇在本机把它的各种变体跑一遍:三种 channel panic 的原文、recover 能救和救不了的情形、select 与阻塞中的发送者、已关闭 channel 的空转陷阱,以及多生产者场景下 30 次运行的对比。
向已关闭的 channel 发送(无 recover,退出码 2):
panic: send on closed channel
goroutine 1 [running]:
main.main()
加 recover 后(退出码 0):recover 抓到: send on closed channel | 是 error: true
关闭已关闭的 channel(无 recover,退出码 2):
panic: close of closed channel
goroutine 1 [running]:
main.main()
加 recover 后(退出码 0):recover 抓到: close of closed channel | 是 error: true
关闭 nil channel(无 recover,退出码 2):
panic: close of nil channel
goroutine 1 [running]:
main.main()
加 recover 后(退出码 0):recover 抓到: close of nil channel | 是 error: truesend on closed、close of closed、close of nil,都是 panic,recover 能抓。
goroutine 里 send on closed channel(退出码 2,用时 0.10 s):
panic: send on closed channel
goroutine 19 [running]:
main.main.func2()
<tmp>/othergo/main.go:7 +0x28
…(共 6 行)main 的 recover 管不到别的 goroutine 里的 panic,整个进程退出。
select { case ch <- 1: ... default: } 对已关闭的 channel(退出码 0,用时 0.12 s):
recovered: send on closed channel
3 个 goroutine 正阻塞在发送上,main 关闭 channel(退出码 0,用时 0.30 s):
sender 0 recovered: send on closed channel
sender 2 recovered: send on closed channel
sender 1 recovered: send on closed channel
从已关闭的缓冲 channel 读(退出码 0,用时 0.10 s):
第 1 次读: v=7 ok=true
第 2 次读: v=0 ok=false
第 3 次读: v=0 ok=false
range 已关闭 channel 的次数: 0select + default 也 panic;关闭时阻塞的发送者全部 panic;读已关闭的 channel 不 panic。
select 中的已关闭 channel(退出码 0,用时 0.30 s):
a 已关闭、没置 nil:100 ms 内 select 空转 1146043 次
a 关闭后置为 nil:100 ms 内 select 进入 1 次已关闭的 channel 在 select 里永远就绪,不置 nil 会吃满 CPU。
消费者收够后 close(data):30 次里 panic 30 次,正常结束 0 次(结束时剩余 goroutine 数 -),其它 0 次 消费者 close(done),生产者 select 退出:30 次里 panic 0 次,正常结束 30 次(结束时剩余 goroutine 数 ['1']),其它 0 次
只让发送方关闭;多个发送方时用 done channel 或 context 通知停下。