bigywater.tech
常见报错 · 43

Go panic: send on closed channel:recover 救不了别的 goroutine,select 带 default 也 panic,多生产者 close(data) 30/30 次 panic、改用 done channel 为 0

“panic: send on closed channel” 的修法常被写成“加个 recover”。这篇在本机把它的各种变体跑一遍:三种 channel panic 的原文、recover 能救和救不了的情形、select 与阻塞中的发送者、已关闭 channel 的空转陷阱,以及多生产者场景下 30 次运行的对比。

30 / 30
“消费者收够后 close(data)” 写法在 4 个生产者下的 panic 次数;改用 done channel 为 0 / 30
3 / 3
关闭 channel 时正阻塞在发送上的 goroutine 全部 panic
1146043
已关闭的 channel 没置 nil 时,select 在 100 ms 内空转的次数

三种 panic

实测 · 本机执行输出
  向已关闭的 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: true

send on closed、close of closed、close of nil,都是 panic,recover 能抓。

别的 goroutine

实测 · 本机执行输出
  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 的次数: 0

select + 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 通知停下。

分步演示

channel 关闭
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64;报错文本都是真实复现时原样捕获的(命令行工具统一用 LC_ALL=C 取英文原文,路径换成 );所有服务都是本机上自己起的、只监听 127.0.0.1,不访问外网;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 go1.27.3 darwin/arm64;每个程序是 go build 后直接运行的小 main.go;空转次数每次运行不同(这次 114 万次),只看数量级
    • 没有验证:用 context 取消代替 done channel 的写法(原理相同,没有单独跑);sync.Once 包一层 close 的写法;errgroup;Go 其它版本的 panic 文字(这里只有 1.27.3);在 recover 里重新 panic 或记录栈的做法;带缓冲 channel 在关闭时缓冲里还有数据的更多组合
    京ICP备2026067502号-1京公网安备11010502063487号