close(ch) 调用 runtime.closechan,先加锁检查 c.closed != 0,若为0则置 c.closed = 1;随后唤醒 recvq 中所有接收者并设 sg.success = false,sendq 中的发送者被唤醒后检测到 closed 立即 panic。
channel 关闭后 closed 字段如何被标记
go 运行时中,close(ch) 最终调用的是 runtime.closechan。该函数第一件关键的事就是检查 c.closed != 0,若已为 1 就直接 panic;否则执行 c.closed = 1。这个字段是 uint32 类型,原子性不强,但靠锁保护:在 lock(&c.lock) 之后、真正清理队列之前完成赋值。这意味着一旦 closed 被设为 1,后续所有读操作(无论是否带 ok)都会立即感知到关闭状态,而无需再加锁读取——因为 recv 函数里会先读 c.closed 判断是否已关,再决定是否走“读缓冲区”或“返回零值+false”路径。
关闭时 recvq 和 sendq 中的 goroutine 怎么处理
关闭不是简单打个标记,而是要唤醒并清理等待者。源码中会遍历 c.recvq(阻塞在接收的 goroutine 队列),对每个 sudog:清空其 elem 内存、设置 sg.success = false、把对应 goroutine 加入待唤醒列表 glist;同理处理 c.sendq,但向已关闭 channel 发送本就会 panic,所以实际进 sendq 的 goroutine 只可能发生在 close 前那一瞬间的竞态窗口——此时它们会被唤醒,并由调度器在恢复执行时检测到 panic 条件(ch.closed 为真且尝试写),从而触发 panic("send on closed channel")。
- recvq 中的 goroutine 唤醒后,
返回零值 +false - sendq 中的 goroutine 唤醒后,下一条指令就是 panic,不会继续执行用户代码
- 没有被唤醒的 goroutine 不会“残留”,因为
glist最终会被goready批量调度
为什么从已关闭 channel 读不 panic,但写会
这是设计上的语义区分:close 表示“不再接受新数据”,但允许消费剩余数据(缓冲区中未读完的、或无缓冲下正被传递中的值)。因此读操作天然安全:有数据就读,没数据就返回零值+false;而写操作违反了“不再接受”的契约,运行时必须立刻阻止。源码里 send 函数开头就检查 if c.closed { panic(...) },且该检查在加锁前就做——避免锁竞争放大开销。注意:这个 panic 发生在**发送方 goroutine 的上下文**,不是接收方,所以堆栈可追溯。
- 关闭后仍能读出缓冲区剩余元素(若有),直到
qcount == 0 - 关闭后首次读空缓冲区,才开始返回零值 +
false - 重复
close或向已关 channel 写,panic 信息完全一样:"close of closed channel"或"send on closed channel"
关闭广播机制的本质是状态 + 唤醒,不是消息投递
所谓“关闭广播”,不是往 channel 里塞了一个特殊信号,而是通过修改 c.closed 状态 + 主动唤醒所有等待 goroutine 实现的。所有正在 的 goroutine 都在 recvq 里挂起,close 一触发,它们就被批量唤醒,并各自按规则返回。这和“往带缓冲 channel 写一个 sentinel 值”完全不同——后者需要额外内存拷贝、类型适配、业务层解析;前者是纯运行时控制流切换。这也是为什么用 channel 关闭来做退出通知(如 done channel)既轻量又可靠:不需要约定值,也不依赖缓冲区大小。
容易被忽略的一点是:如果某个 goroutine 正在执行 select,且多个 case 都 ready(比如同时有 和 ),关闭 channel 并不会强制它选中那个 case——select 的随机公平性仍然生效。只有当它**真正执行到该 case 的接收操作时**,才会因 channel 已关而立即返回。











