channel的send/recv非简单队列操作,而是运行时依状态动态决定阻塞、唤醒或直接数据交接;无缓冲channel因dataqsiz==0,send/recv必触发goroutine调度。

channel 的 send 和 recv 不是简单地“往队列塞/取”,而是由运行时根据当前状态动态决定是否阻塞、唤醒或直接交接数据。
无缓冲 channel 的 send/recv 必然触发 goroutine 调度
无缓冲 channel 没有 buf,dataqsiz == 0,所以每次 ch 或 都必须配对完成:一方发送时,另一方必须已在 recvq 等待;反之亦然。
- 若
recvq非空,send直接把值拷贝给等待的sudog,唤醒接收方 goroutine,不修改qcount,也不动sendx/recvx - 若
recvq为空,当前 goroutine 会封装成sudog加入sendq,然后调用gopark挂起自己 —— 这是真实调度点,不是“忙等” - 同理,
recv在sendq非空时直接取值唤醒发送方;否则挂起进recvq
这意味着:两个 goroutine 对无缓冲 channel 的收发顺序不可预测,谁先被调度谁先尝试操作,但最终必有一方阻塞,直到另一方抵达。
有缓冲 channel 的 send/recv 多数情况不触发调度
只要缓冲区未满(qcount )或未空(qcount > 0),send 和 recv 就只操作环形缓冲区和计数器,全程持有 lock,不涉及 goroutine 挂起或唤醒。
-
send:将值拷贝到buf[sendx],sendx = (sendx + 1) % dataqsiz,qcount++ -
recv:从buf[recvx]拷贝值,recvx = (recvx + 1) % dataqsiz,qcount-- - 仅当缓冲区满时
send才入sendq;空时recv才入recvq - 环形缓冲区是固定大小原始内存(
mallocgc分配),不逃逸,避免 GC 扫描开销
close 后的 recv 行为容易误判
关闭 channel 只置 closed = 1,并唤醒所有 recvq 上的 goroutine。后续 不 panic,但返回零值 + ok == false —— 这个 ok 必须显式检查。
- 错误写法:
for { x := → 缓冲读完后持续拿到零值,逻辑崩溃 - 正确写法:
for x, ok := 或for range ch -
close(nil)会 panic,和map一样,使用前应确保 channel 已make - 向已关闭 channel
send立即 panic:send on closed channel
select 中的 send/recv 调用的是不同函数
单独写 c 走 chansend;在 select 里则走 selectnbsend(非阻塞 send)或 selectnbrecv(非阻塞 recv)—— 它们会在无法立即完成时快速失败,不挂起 goroutine。
- 有
default的select是真非阻塞,适合轮询或超时控制 - 没有
default的select仍可能阻塞,只是多路复用多个 channel -
select的 case 是**伪随机公平选择**,不是 FIFO,也不是优先级排序
真正难处理的,是那些看似“没阻塞”却因环形缓冲区索引错位(sendx == recvx 但 qcount == 0)或 sudog 状态残留导致的调度延迟 —— 这些细节藏在 runtime/chan.go 的几十行条件判断里,调试时得看汇编或加 trace 才能确认实际路径。











