select是Go中唯一能同时等待多个channel操作的阻塞式分支选择原语;必须注意默认分支缺失导致永久阻塞、nil channel被忽略、case执行伪随机及timeout需配合context避免泄漏。

select 是 Go 中唯一能同时等待多个 channel 操作的原语,但它不是“轮询”或“事件监听器”,而是阻塞式、非抢占的分支选择——用错场景或忽略默认分支,很容易卡死或漏事件。
select 会永久阻塞,除非至少有一个 case 可立即执行
这是最常踩的坑:当所有 case 的 channel 都没数据、也没 default 时,select 会一直挂起,整个 goroutine 冻结。
- 想“非阻塞尝试收发”,必须加
default分支(哪怕只写default: continue) - 想“等任意一个就返回”,不加
default是对的;但要确保至少一个 channel 有数据,或有 goroutine 在往其中写 - 如果所有 channel 都是 nil,
select会永远阻塞——这常发生在未初始化的 channel 变量上
nil channel 在 select 中永远不可读/写
向 nil channel 发送或接收,会永久阻塞;在 select 里,对应 case 直接被忽略。这可以用来动态开关监听路径,但容易误判为“channel 没数据”。
- 初始化后赋值为
nil的 channel,在select中等价于该case不存在 - 比如用
ch := make(chan int)后又ch = nil,再放进select就不会参与调度 - 调试时打印
ch == nil比看是否“没反应”更可靠
case 执行顺序不保证,但 default 优先级最高
Go 运行时会在所有可执行的 case 中**伪随机**选一个执行——不是 FIFO,也不是按代码顺序。只有 default 是例外:只要存在且可执行,它一定被选中。
- 不要依赖
case的书写顺序来控制逻辑优先级 - 如果需要严格优先级(比如先处理退出信号),把高优 channel 放进独立
select,或用default+ 显式轮询 - 多个
case同时就绪时,无法预测哪个运行,这对超时控制、重试逻辑影响很大
timeout 和 cancel 场景下,time.After 和 context.Done 配合最稳
单独用 time.After 做超时有内存泄漏风险(定时器不会被 GC,直到触发),而 context.WithTimeout 的 Done() channel 更可控。
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
select {
case val := <-dataCh:
fmt.Println("got", val)
case <-ctx.Done():
fmt.Println("timeout or cancelled")
}
-
time.After适合一次性短超时;长期运行的 goroutine 必须用context -
ctx.Done()关闭后,再次读取会立即返回,适合反复复用的 select 循环 - 别在循环里重复调用
context.WithTimeout,会导致新定时器堆积
真正难的不是语法,而是判断哪些 channel 必须始终有效、哪些可以临时置 nil、哪些事件必须零丢失——这些得靠业务语义定,select 本身只负责公平调度。

















