select 的随机选择是怎么实现的?default 分支有什么用?
结论先行
select 同时等待多个 channel 操作:当多个 case 同时就绪时,运行时按均匀概率随机挑一个执行;没有 case 就绪且无 default 时当前 goroutine 挂起等待;有 default 时 select 变为非阻塞,立即执行 default。这既是公平性的来源,也是“非阻塞尝试收发”与“超时兜底”的基石。
要点
- 随机性来源:编译器生成的 select 先做 pollorder 随机排列,再逐个探测 case;多个就绪时选中的顺序因此随机,避免某个 case 长期不被选中。
- 挂起机制:所有 case 都不就绪时,把当前 G 同时挂到所有相关 channel 的等待队列(先按 lockorder 排序加锁防止死锁),任一 case 可用即被唤醒。
- default 语义:有 default 时“全不就绪”就立刻走 default,等价于 try-lock/try-send,不会阻塞。
- nil channel:在 select 里永不就绪,把某个 ch 置 nil 即可临时禁用对应 case,无需重写代码。
- 常见模式:for-select 主循环、带超时等待(select + time.After)、把 ctx.Done() 与业务收发并列实现可取消。
- 陷阱:循环里每次 select 用 time.After 都会新建定时器且要到触发才释放,长时间循环会累积;应复用 time.Timer 并 Stop/Reset。
- 空 select:select{} 没有任何 case,会永久阻塞,常用于让 main 一直等待后台任务或信号。
示例
ch1, ch2 := make(chan int), make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
for i := 0; i < 2; i++ {
select { // 两个都就绪时随机选一个
case v := <-ch1:
fmt.Println("ch1", v)
case v := <-ch2:
fmt.Println("ch2", v)
}
}
var ch chan int // nil,永不就绪
select {
case v := <-ch:
fmt.Println(v)
default: // 不会阻塞,立即执行
fmt.Println("没有数据,走 default")
}
常见追问/记忆点
- 追问:select 公平吗?答:运行时随机打乱顺序加随机起点,尽量避免单个 case 被饿死,但不做严格轮询保证。
- 追问:default 的代价是什么?答:放弃“等待就绪”的能力,没就绪就立刻返回,适合探测而非同步等待。
- 追问:select 里能放发送 case 吗?答:能,case ch <- v 同样参与就绪判定与随机选择。
- 记忆:随机 + 阻塞 + default 非阻塞;nil channel 是天然的“禁用开关”。