Go 的 channel 是一套带锁、带等待队列、带环形缓冲区的协作状态机,其行为由 hchan 结构体字段、goroutine 状态和操作类型共同决定;buf 仅在有缓冲时有效,sendx/recvx 为模运算索引,qcount 原子维护实际已用容量,recvq/sendq 为阻塞时临时挂载的 sudog 链表,closed 关闭后读可继续、写则 panic。

Go 的 channel 不是简单队列,而是一套带锁、带等待队列、带环形缓冲区的协作状态机;它的行为完全由 hchan 结构体字段 + 当前 goroutine 状态 + 操作类型(send/recv)共同决定。
hchan 结构体各字段的实际作用
源码中 hchan 是 runtime 内部结构,用户不可见,但每个 make(chan T, N) 都会分配一个。关键字段不是独立存在,而是协同控制运行路径:
-
buf仅在dataqsiz > 0时非 nil,指向一块连续内存;若为 nil(即无缓冲 channel),所有收发必须配对才能完成 -
sendx和recvx是模dataqsiz的索引,不是绝对地址;当dataqsiz == 0时,这两个字段恒为 0,不参与数据拷贝 -
qcount是唯一实时反映缓冲区“已用容量”的字段;它不等于sendx - recvx(因可能绕圈),而是由 runtime 在每次成功收发后原子更新 -
recvq和sendq是sudog链表,只在阻塞时被写入;一旦 goroutine 被唤醒,对应sudog就从链表摘除并复用,不长期驻留内存 -
closed是 uint32,用原子操作读写;关闭后recvq中的 goroutine 会被批量唤醒,但sendq中的 goroutine 会被直接 panic(panic: send on closed channel)
channel 收发操作如何触发状态迁移
一次 ch 或 <code> 不是原子指令,而是分阶段进入不同分支,取决于当前 <code>hchan 状态和是否有等待者:
- 若 channel 无缓冲(
dataqsiz == 0)且recvq非空:直接从 sender 栈拷贝v到 receiver 栈,唤醒recvq头部 goroutine,不经过buf - 若 channel 有缓冲、
qcount :将 <code>v拷贝进buf[sendx],sendx自增,qcount加 1,全程不阻塞 - 若 channel 有缓冲但已满(
qcount == dataqsiz)且sendq为空:当前 goroutine 封装为sudog,挂入sendq尾部,然后 park(让出 M/P) - 若 channel 已关闭且正在接收:立即返回零值,
ok == false;若正在发送:触发 panic,不进入任何队列
为什么 close(ch) 后还能从 ch 读取?
关闭操作本身只改 closed = 1 并唤醒 recvq 中所有 goroutine,但不清理 buf 或重置 qcount。所以:
- 只要
qcount > 0,后续仍能从 <code>buf[recvx]取出剩余数据,recvx和qcount正常递减 - 当
qcount降为 0 后,再读才返回零值 +ok == false - 注意:
close()只能由 sender 调用;对只读 channel()调用 <code>close会编译报错
容易被忽略的调度细节
goroutine 阻塞在 channel 上时,不是“睡着了就不管”,runtime 会在关键节点介入:
- 被唤醒的 goroutine 不一定立刻执行:它被 ready 进 runqueue 后,仍需等待 P 调度;若此时 P 正忙,延迟可达毫秒级
-
sudog.elem指针指向的是 sender/receiver 栈上的变量地址,不是堆;若 goroutine 在阻塞期间栈发生 growth/shrink,runtime 会自动更新该指针 - select 多路分支中,若多个 case 同时就绪,Go 采用**伪随机**选择(实际是遍历 case 数组下标取模),不是 FIFO 也不是优先级


















