go channel读写不走双端队列,而是用独立索引sendx/recvx控制环形缓冲区,qcount为二者差值;无缓冲时直接goroutine配对交接,qcount恒为0;recvq/sendq是独立fifo等待链表,非环形结构。

channel 读写操作不走双端队列
Go 的 channel 内部确实用循环队列(buf)存数据,但它的调度逻辑不是“双端队列”(deque)那种两端都可入可出的结构。它严格遵循 FIFO,且读写位置由两个独立索引控制:sendx 和 recvx,它们只单向递增、越界回绕,不共享头尾指针。
常见误解是认为“先写后读就一定按顺序收”,其实更关键的是:谁在等、谁先被唤醒。
- 无缓冲
channel:发送和接收必须配对发生,没有“排队”过程,直接 Goroutine 交接 ——sendx和recvx始终相等,qcount永远为 0 - 有缓冲
channel:写入时只动sendx,读取时只动recvx;两者差值就是qcount,即当前队列长度 -
recvq和sendq是两个独立的等待链表(waitq),不是环形结构,也不共享任何索引
recvq / sendq 队列的唤醒顺序是 FIFO
当一个 channel 缓冲区空,而有多个 Goroutine 在 等待时,它们会被挂进 <code>recvq;一旦有数据写入,runtime 会从 recvq 头部取出第一个 Goroutine 唤醒 —— 这个顺序由链表插入顺序决定,不是优先级或时间戳。
同理,缓冲区满时阻塞的发送者进入 sendq,后续有接收者到来,也是从 sendq 头部唤醒。
- 不能靠“写得快就先被读”,得看 Goroutine 进入
recvq的先后 - 用
select多路等待时,如果多个 case 同时就绪,Go 会**伪随机选择**一个,不保证 FIFO -
recvq/sendq中的 Goroutine 被封装为sudog,其next/prev字段构成单向链表,无跳转或优先级字段
为什么你看到的“顺序执行”其实是调度巧合
很多示例代码里多个 Goroutine 按启动顺序读取 channel,看起来像“先进先出”,但这只是因为它们几乎同时启动、几乎同时阻塞、被 runtime 按插入顺序唤醒 —— 不是 channel 保证了执行顺序,而是调度器没打乱。
真实并发下,只要存在调度延迟、GC 抢占、P 切换,这个“顺序”就会打破。
- 别依赖 Goroutine 启动顺序来推断
channel消费顺序 - 如果需要强顺序(比如日志归并、状态流转),应显式用串行化手段:单个消费者 +
for range ch,或加锁控制消费逻辑 - 用
time.Sleep强制错开启动时间,反而更容易暴露非确定性行为
缓冲区满/空时的阻塞行为直接影响调度路径
是否阻塞,决定了 Goroutine 是立刻返回、还是被挂起进 recvq/sendq,这会改变整个调度树的分支。
例如:
ch := make(chan int, 1) ch <p>此时若另一个 Goroutine 执行 <code>,它不会从 buf 取数再唤醒 sender;而是直接从 <code>sendq</code> 拿走 <code>sudog</code>,把 <code>2</code> 从 sender 栈拷贝过来,再唤醒 sender —— 整个过程绕过 <code>buf</code>。</code></p>
- 有缓冲 channel 并不总是用到
buf:当sendq或recvq非空,runtime 会直连 Goroutine,跳过缓冲区 - 这意味着即使
dataqsiz > 0,实际内存中也可能长期只有零个元素在buf里 - 所以别假设“缓冲区大小 = 实际排队能力”,它只是影响阻塞触发点的阈值
真正决定行为的是 hchan 结构体里那几个字段的实时状态,而不是抽象的“队列模型”。调试时用 runtime.ReadMemStats 看不到这些,得靠 pprof 或 go tool trace 观察 Goroutine 状态跃迁。











