Go 的 select 不是 I/O 多路复用,仅用于 goroutine 内 channel 通信协调;它不操作文件描述符,不提升 TCP 连接上限,瓶颈在 netpoll 和调度器;滥用轮询或 time.After 会加重调度与 GC 压力。

select 不是 I/O 多路复用,只是 channel 协调语法
很多人看到“多路复用”就默认它像 epoll 或 kqueue 那样直接操作文件描述符——不是。Go 的 select 完全不碰底层 socket,它只监听 channel 状态。它的作用域仅限于 goroutine 内部的通信协调。
这意味着:你不能靠堆 select 分支来提升 TCP 连接数上限;真正压测时瓶颈在 netpoll、goroutine 调度器和内存分配,而不是 select 本身。
- 想处理海量连接?用
net.Conn+runtime_pollWait底层机制,select只负责把读/写/关闭事件转成 channel 消息 - channel 背后仍是 runtime 的 poller,但
select语句本身不参与系统调用 - 滥用
select做轮询(比如每毫秒扫一次几十个 channel)会显著增加调度器负担,GC 压力也会上升
time.After 在循环里等于内存泄漏
写 case 看似简洁,但在 for 循环里每次都会新建一个 <code>timer 对象。如果该分支长期不被选中(比如网络卡顿、下游响应慢),这些 timer 就滞留在堆上,无法被 GC 回收。
这不是理论风险——线上服务跑几天后 runtime.MemStats.HeapObjects 就会明显上涨。
立即学习“go语言免费学习笔记(深入)”;
- 高频重试、心跳检测、流式消费等场景,必须改用
time.NewTimer -
timer.Stop()要在select退出后立刻调用,哪怕进了default分支也不能漏 - 更稳妥的做法是统一用
context.WithTimeout,让取消信号自然流入http.Client.Do、sql.DB.QueryContext等原生支持 context 的 API
default 不是超时,是“此刻无事可做”
把 default 当作超时分支,会导致两个严重后果:CPU 空转 + 逻辑错乱。它根本不启动任何计时器,只是告诉运行时:“现在所有 channel 都没准备好?那别等了,马上执行我。”
典型误用:在 for 循环里写 select { default: doWork() },结果 doWork() 每毫秒被调上百次,goroutine 占满一个 OS 线程。
-
default合适的用途:快速探测 channel 是否有数据(如select { case x := ) - 真要超时控制,必须用可接收的定时通道:
time.After(单次)、time.NewTimer(复用)、或ctx.Done() - 注意:
nilchannel 在select中永远不就绪,且不会触发 panic,但会让对应case彻底失效——常被用来动态开关分支
多个 case 同时就绪时,执行顺序完全随机
哪怕你把退出信号 quitCh 放在第一个 case,只要它和数据通道 dataCh 在 select 判定瞬间都已就绪,Go 就可能挑后者执行。这不是 bug,而是防饥饿的刻意设计。
调试时多跑几次,输出顺序跳变,就是被随机调度了。
- 不能靠书写顺序控制优先级;也不能靠发送时间先后判断哪个先被选中
- 需要严格响应顺序(比如必须先处理 cancel)?拆成嵌套
select,或在外层加if select { case 判断 - case 右边的函数(如
case )会在进入 <code>select前求值,耗时操作必须提前算好
实际写的时候,最容易被忽略的是:case 表达式求值时机、nil channel 的静默失效、以及随机调度带来的逻辑不确定性——这些不是边缘情况,而是日常并发代码里最常引发隐性 bug 的点。


















