Go语言select语句中Channel缓冲区与Pending队列的优先级问题
为什么你的Go select测试结果和源码逻辑看似矛盾?
你误解了select源码中recv分支的具体行为——它并不是在所有情况下都会优先接收pending发送者的值。核心原因在于:只有当缓冲区为空时,才会直接接收发送者的值;而当缓冲区已满时,recv分支的逻辑是先取出缓冲区头部的元素返回给接收者,再把发送者的值放入缓冲区。
一、拆解你的测试场景流程
你的测试代码中发生了这些事:
- 创建了大小为3的缓冲channel,填满了
1、2、3,此时缓冲区处于满状态; - 启动goroutine尝试发送
4,因为缓冲区已满,这个goroutine被挂到channel的sendq等待队列; - 主goroutine执行
select接收channel的值。
二、解析recv分支的实际处理逻辑
你贴的select源码只展示了pass1阶段的检查逻辑,但goto recv后的处理代码(你没贴出来)才是关键:
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { if c.dataqsiz == 0 { // 无缓冲channel:直接把发送者的值复制给接收者 if ep != nil { typedmemmove(c.elemtype, ep, sg.elem) } } else { // 缓冲channel且已满:先取缓冲区头部元素给接收者,再把发送者的值放入缓冲区 qp := chanbuf(c, c.recvx) if ep != nil { typedmemmove(c.elemtype, ep, qp) // 复制缓冲区头部的1到你的val变量 } typedmemmove(c.elemtype, qp, sg.elem) // 把发送者的4放入缓冲区 c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.sendx = c.recvx // 缓冲区已满,sendx和recvx指向同一位置 } sg.success = true goready(sg, skip+1) // 唤醒发送goroutine,它会继续执行 unlockf() }
在你的场景中,缓冲区已满,所以recv分支做了两件核心事:
- 把缓冲区头部的
1复制给你的val变量; - 把发送goroutine的
4放入缓冲区尾部; - 唤醒发送goroutine(此时缓冲区有了一个空位)。
这就是为什么你看到的结果是receive val is 1,而不是预期的4。
三、什么时候才会优先接收pending发送者的值?
只有当缓冲区为空时,recv分支才会直接接收发送者的值。比如修改你的测试代码:
func main() { var mark chan int c := make(chan int, 3) // 缓冲区为空 go func() { c <- 4 }() time.Sleep(time.Millisecond * 10) select { case val := <-c: fmt.Println("receive val is ", val) // 输出4 case _ = <-mark: fmt.Println("block chan") } }
此时缓冲区为空,发送goroutine被挂到sendq,select的recv分支会直接把发送者的4复制给接收者,结果就是4。
四、总结select中caseRecv的优先级逻辑
对于caseRecv,select的pass1阶段会按以下顺序检查:
- 如果sendq有等待的发送者:
- 若缓冲区为空:直接接收发送者的值;
- 若缓冲区已满:取出缓冲区头部的值返回,再把发送者的值放入缓冲区;
- 如果缓冲区有数据:直接取出缓冲区头部的值返回;
- 如果channel已关闭:返回零值;
- 否则:进入阻塞等待。
这样设计的核心目的是保证缓冲channel的FIFO语义——缓冲区的元素必须按发送顺序被接收,不能被pending的发送者插队。
内容的提问来源于stack exchange,提问作者HuDahai
相关产品推荐
相关产品推荐

