Go的select在多个case同时就绪时伪随机选择,底层通过哈希打乱索引后线性扫描实现公平调度,旨在避免饥饿和隐式顺序依赖;default仅兜底,不参与随机选择。
go 的 select 在多个 case 同时就绪时,确实会伪随机选择一个执行——这不是巧合,而是运行时明确实现的公平调度策略。
底层怎么做到“随机”的
它不按代码书写顺序扫描,也不轮询,更不是优先选第一个;而是把所有就绪的 case 索引打乱(用哈希 + 位运算做伪随机重排),再线性遍历选第一个。这个过程在每次 select 执行时都重新发生。
- 目的是防止 goroutine 饥饿:避免某个
case总是被跳过 - 消除隐式顺序依赖:如果靠写法顺序决定行为,改个
case位置就可能破坏逻辑 - 不可预测 ≠ 不可控:只要控制好哪些
case就绪,就能控制行为边界
为什么你常看到“总走第一个 case”
那不是 select 按顺序选,而是你没让多个 case 真正同时就绪。常见诱因:
- 用无缓冲 channel,但发送方 goroutine 启动/执行时机不确定,导致只有一个
case实际就绪 - 某个
case是 send 操作,而接收方还没 ready,该case就不算就绪 - 加了日志或调试语句,改变了 goroutine 调度节奏,让原本接近同时的就绪变成有先后
验证方法:用 make(chan int, 1) 预填值,确保收发都立即就绪,再跑 1000 次循环统计分布——结果会接近均匀(±5% 内)。
default 分支不会干扰随机性,但会改变行为模型
default 的作用是兜底,不是参与随机选择。它的存在让整个 select 变成非阻塞,但不影响“多个就绪 case 间如何选”这一层逻辑:
- 只要有任意一个
case就绪(哪怕只有一个),default就完全不执行 - 只有所有
case都不就绪时,才进default - 别在
default里放“重试逻辑”并指望下次命中特定case——因为下一次就绪状态可能完全不同
别用 select 实现优先级,除非你清楚代价
想让 ch1 优先于 ch2?直接写 select 不行。常见误操作:
- 把
ch1放第一个case,以为能“优先”——实际无效,且误导维护者 - 在
default里 sleep 后重试——增加延迟、浪费资源、仍不保证顺序
真要优先级,得用显式状态控制,比如:if ch1 != nil && len(ch1) > 0 { ... } else if ch2 != nil && len(ch2) > 0 { ... },但注意这和 select 的阻塞/非阻塞语义不同,且需自行处理竞态。
最易被忽略的一点:随机性只在“多个就绪”时生效;而“就绪”的判定本身依赖 channel 缓冲、goroutine 调度、是否关闭等细节——这些才是实际编码中真正需要反复验证的部分。


















