想象你是万达广场新上任的值班经理小李。你面前有三个服务窗口:退换货窗口、咨询窗口、投诉窗口。三个窗口随时可能有人按铃,而你只有一个人——不可能死盯一个窗口而错过另外两个。你的职责是“同时盯住”所有窗口:哪个窗口有人就处理哪个,若多个窗口同时按铃,就随机挑一个(绝不能偏心),处理完再回到观望状态。这套“一人盯多窗”的能力,正是 Go 语言中 select 多路复用的精髓:让一个 goroutine 同时监听多个 channel,谁有数据就处理谁。 Go 的并发模型里,goroutine 是轻量级“值班经理”,channel 是工位之间的“对讲机”。当一个 goroutine 需要同时接收多个 channel 的消息时,select 就是专门为它设计的调度面板。这篇文章会沿着小李的值班经历,带你搞懂 select 的基本概念、语法结构、多路监听原理、超时控制、for-select 循环、非阻塞操作,以及用 reflect 构建动态 case 的进阶玩法。学完之后你会发现:处理多个 channel,就像小李盯窗口一样,有条不紊。
对 AI 从业者的价值:AI 服务常需并发接收推理请求、监听上游模型结果、处理心跳与超时——select 是构建这类高并发反应式系统的钥匙,也是 Go 后端与云原生岗位面试的高频考点。
可视化演示
演示Go语言select多路复用机制:多channel同时监听、for-select事件循环、超时心跳控制和default非阻塞操作,通过交互式动画展示channel数据流动与select调度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
select多路监听 同时监听多个channel 随机选中就绪case 阻塞直到有case就绪 select是Go并发编程的核心原语
基本概念
小李的第一天并不顺利。他试着按顺序盯窗口:先盯退换货窗口,没人;再盯咨询窗口,也没人;好不容易等到咨询窗口来人,处理完一回头,投诉窗口已经炸锅了——退换货窗口的客户也等得不耐烦走了。“挨个巡逻”的关键问题在于:你在一个窗口前等待时,其他窗口的消息全部错过了。小李需要的不是“轮询”,而是“同时感知所有窗口”。
Go 并发编程中也有同样的困境。假设一个 goroutine 要从 ch1、ch2、ch3 三个 channel 接收数据,最直觉的写法是先 v := <-ch1 再 v := <-ch2——但第一个接收操作会阻塞住 goroutine,如果 ch1 迟迟没有数据,ch2 和 ch3 即使已经送达,这个 goroutine 也看不见。所以 Go 提供了 select 关键字:它像小李的培训手册一样,要求 goroutine“一次盯住所有 channel”。
select 的用法和 switch 很像,但每个 case 都必须是 channel 的发送或接收操作。它有两个关键语义:第一,select 会阻塞,直到某个 case 的 channel 操作就绪;第二,若有多个 case 同时就绪,Go 会伪随机地选中一个执行,保证每个就绪的 channel 都有均等机会。下面这段代码演示了同时监听两个 channel:
小李最终在培训手册里学到一条铁律:“不要挨个巡逻,要同时盯住所有窗口;多个窗口同时按铃时,闭眼随机挑一个。”这正是 select 的全部哲学——多路监听、阻塞等待、随机公平。
- 就绪标记$r_i$ 表示第 i 个 case 对应的 channel 是否可收发,1 表示就绪。
- 就绪集合把所有就绪的 case 下标收集起来,构成一个集合。
- 集合大小用绝对值符号表示就绪 case 的总个数,记为 $M$。
- 均匀概率随机选择要求每个就绪 case 的被选概率相等。
select {case v := <-ch1: // 处理来自 ch1 的数据case v := <-ch2: // 处理来自 ch2 的数据}
select语法
小李在值班室挂了一块白板,上面写着值班规则:“如果退换货窗口有人 → 处理退换货;如果咨询窗口有人 → 处理咨询;如果投诉窗口有人 → 处理投诉;如果都没人 → 先巡视商场。”把这块白板翻译成代码,就是 select 语句。白板上的每条“如果”就是一个 case,最后一行“都没人”就是 default。
select 的语法由一个 select 关键字和若干 case 组成。每个 case 都绑定一个 channel 操作:可以是接收 case v := <-ch,也可以是发送 case ch <- v。与 switch 最大的不同是,select 不会自上而下依次判断,它的判定依据是“哪个 channel 操作可以立即执行”。谁就绪,谁胜出;多个就绪,随机胜出。
default 分支是“都没人”时的兜底。当所有 case 都未就绪时,select 不会傻等,而是立即执行 default。如果没有 default 且没有任何 case 就绪,select 会一直阻塞当前 goroutine,直到某个 channel 可用。注意,select {} 是一个没有 case 的 select,它会永久阻塞,常用于“不希望 goroutine 退出”的后台任务。
白板上的值班规则神奇地变成了 Go 代码,小李突然明白:原来“值班规则”就是 select 语法。“所有人都没来”的场景,就是 default 分支的价值——它让经理不傻等、永远有事可做。
- 就绪集合统计所有可以立即执行的 case。
- 有就绪分支只要 C 非空,就从里面选一个 k 执行。
- default 兜底C 为空但有 default 时,直接进入 default。
- 否则阻塞既无就绪 case 也无 default,只能挂起等待。
多路监听
小李注意到一个细节:如果退换货窗口和投诉窗口同时按铃,他不能总是先处理退换货——不然投诉窗口的客户天天排不到队,迟早投诉到总部。值班守则里有一条“公平原则”:同时有多个窗口需要服务时,随机挑一个。Go 的 select 在底层也遵循同样的原则,而且实现得更严谨。
在 Go 编译器看来,select 并不是一堆 if-else 的简易包装。它会被编译为一次对运行时函数 runtime.selectgo 的调用。selectgo 的执行分两个阶段:第一阶段先对全部 case 做一次快速的“就绪扫描”,看有没有 channel 已经可收发;如果找到了若干个就绪 case,就随机选一个执行。第二阶段是当没有任何 case 就绪时,把当前 goroutine 挂起,并把这个 goroutine 的等待信息注册到每个 channel 的收发队列上,直到某个 channel 出现数据再唤醒它。
selectgo 对就绪 case 的处理用到了伪随机打乱(shuffle):先把 case 顺序随机置换,再从前往后找第一个就绪的 case。这正是“公平原则”的来源——它保证每个就绪 case 长期来看被选中的概率基本一致,不会被排在前面的 case“饿死”。同时,selectgo 的单次扫描是 O(n) 的,n 是 case 数量,所以别把 select 写成上千个 case。
小李把“公平原则”写进了值班手册。selectgo 的随机打乱,就像小李闭眼转一圈再指一个窗口——不是刻意挑选,而是设计上就不让任何窗口被冷落。
- 打乱顺序shuffle 将 case 数组随机置换,得到新数组。
- 寻找就绪按新顺序找到第一个 r=1 的 case。
- 执行选中命中后执行对应 case 的分支逻辑。
超时控制
小李不能无限期地盯窗口。商场有规定:如果 3 分钟内没有窗口按铃,经理必须去巡场检查消防安全。于是小李把手表定了个 3 分钟闹钟——闹钟一响,不管有没有客户,他都要去做下一件事。这个“闹钟”,就是 Go 标准库里的 time.After。
time.After(d) 接收一个时间长度 d,返回一个 channel。经过 d 时长后,这个 channel 会收到一条当前时间数据。把它放进 select 的一个 case 里,就变成了一个“定时开关”:如果其他 case 在 d 时间内都没有就绪,select 就会走 time.After 的 case,从而实现超时保护。
与超时对应的是心跳。time.Tick(d) 会返回一个周期性发送数据的 channel,每隔 d 时长就“叮咚”一次。在 for-select 循环里监听 time.Tick 的 channel,就能周期性地执行清理、上报状态、发送探活包等操作。注意区分:time.After 触发一次就结束,适合一次性超时;time.Tick 可持续使用,适合周期性心跳。在 for 循环里反复调用 time.After 会产生大量一次性定时器,建议改用 time.NewTicker。
小李的闹钟和 Go 的 time.After 异曲同工——都是给“无限等待”加一条边界。经理不会永远傻站,goroutine 也不该无限阻塞;超时和心跳,让系统在异常时仍有一条逃生通道。
- After 单次触发在 t0 时刻调用,等到 t0+d 时刻 channel 收到数据。
- Tick 周期触发从调用时刻起,每隔 d 时长收到一次数据。
- 超时组合把 after channel 当作一个普通 case 放进 select。
for-select
小李不是处理完一个客户就下班。他是全天候值班:处理完投诉窗口的事,立刻回到观望台,继续盯三个窗口——处理、回归、再处理、再回归。这种“永远在路上的循环”,就是 for-select 模式。这也是 Go 并发程序里出现频率最高的代码形态之一。
for-select 的骨架很固定:外层是一个无限 for,内层是 select。select 处理完一个事件后,循环继续;想让 goroutine 退出,通常引入一个专门的 stopCh,在 case 中 return。下面是一个标准写法:
for-select 有两点容易踩坑。第一,case 里的 break 只能跳出 select,跳不出外层 for,必须用带标签的 break 或 return 才能退出循环。第二,多个 goroutine 可以同时跑 for-select 消费同一个 channel 池,这是 worker 池的常用写法——每个 goroutine 都是一个“值班经理”,大家轮流处理窗口事件。另外,如果 select 永远没有 case 就绪,for-select 会一直阻塞但不会烧 CPU,因为 goroutine 被挂起而非忙等。
小李的“处理→回归”节奏,正是 for-select 的内核。一个成熟的 goroutine 不是处理一次就结束,而是像值班经理一样:服务完一个窗口,立刻回到观测位——直到有人喊“下班”(stopCh),才退出循环。
- 事件循环每轮迭代负责处理一个 channel 事件。
- 处理数据从 case 取出数据,交给处理函数。
- 终止条件stop 信号是唯一的退出出口。
for { select { case v := <-ch1: handle(v) case <-stopCh: return }}
非阻塞操作
小李偶尔也会遇到这种情况:他正在巡查锅炉房,对讲机里传来消息“三个窗口目前都没人”。这时他需要立刻跑回去吗?不需要。他会快速扫一眼每个窗口——确认没人按铃,就继续巡场。这种“瞄一眼,没人就走”的操作,就是 Go 里的非阻塞操作。
默认情况下,select 会阻塞到某个 case 就绪。但加上 default 分支后,语义完全变了:如果所有 case 的 channel 操作都未就绪,select 不等待,立即执行 default。这就实现了“尝试型”操作——尝试发送、尝试接收,通道忙就干别的。代码写起来非常直观:
非阻塞操作在真实系统中很有用:日志缓冲满了就丢弃而不是卡住生产者;更新缓存时不强制等待旧数据;在事件循环里快速轮询多个 channel 是否有新数据。但要注意,非阻塞并不意味着更好——如果业务要求“必须收到”某个消息,还是要用阻塞模式。default 是“有余力时做的额外动作”,而不是“任务完成的保证”。
小李的“瞄一眼就走”,在 Go 里正是 default 的智慧。它让 goroutine 不必死等,可以在繁忙的系统中转身去做优先级更高的事——这就是非阻塞思想在 Go 并发中的体现。
- C 为空没有任何 case 的 channel 可以立即执行。
- 有 default不阻塞,走兜底逻辑,goroutine 继续运行。
- 无 default挂起当前 goroutine,等待 channel 就绪。
select {case ch <- v: // 发送成功default: // ch 已满或未就绪,不阻塞,走其他逻辑}
动态select
小李升职为区域经理,管理的窗口不再固定:今天 3 个,明天 5 个,节假日还要临时加开 VIP 窗口。白板上的值班规则如果每次改窗口都要重新印刷,根本来不及。他需要一张能随时增删的“动态窗口表”。
静态 select 的 case 列表在编译期就写死了,无法在运行时增删。Go 的 reflect 包提供了 reflect.Select 和 reflect.SelectCase,可以在运行时动态构造 case 列表。每个 reflect.SelectCase 有三个字段:Dir(方向:发送、接收或 default)、Chan(对应的 channel 值)、Send(要发送的数据)。把 SelectCase 切片传给 reflect.Select,它会返回三个值:被选中的 case 下标、接收到的值、以及是否成功。
典型场景:一个连接池管理器,goroutine 数量不变,但连接不断增删;或者一个事件注册中心,模块启动时注册自己关心的 channel,关闭时注销。动态 select 让这些“运行时才知道监听谁”的需求变成现实。代价也很明显:reflect 操作有运行时开销,在每秒百万事件的高频路径上,应尽量使用静态 select。
小李的“动态窗口表”让他从三个窗口的值班经理,成长为管理任意数量窗口的区域经理。动态 select 给了 Go 程序员同样的能力——用反射的灵活性换取运行时动态性,代价是性能,所以要把它用在真正的刀刃上。
- 动态构造每一项目录包含方向、channel 和发送值。
- 传入 Select把整个列表交给 reflect.Select 调度。
- 返回下标chosen 是被选中 case 在列表中的位置。
AI 应用场景
智能客服网关的多模型“抢答”编排
阶段:推理服务部署与调度
解决的问题:智能客服要先调用意图识别、实体抽取、情感分析三个模型,再把结果汇总给对话策略模块。传统做法是按顺序串行调用,总延迟等于三个模型延迟之和(比如 200ms+180ms+150ms=530ms),用户等得着急。用 select 同时监听三个模型的结果 channel,总延迟就变成三者最大值约 200ms,还能在硬超时内先返回已有部分结果,实现“有损但及时”的响应。
具体案例:某银行智能客服网关中,一个 goroutine 用 for-select 同时监听三个 resultCh 和一个 timerCh(由 time.After(300ms) 产生)。任意两个模型先返回即可组装答案,超时则把已返回结果降级返回。网关压测显示 p99 延迟从 620ms 降到 280ms,超时兜底避免了雪崩。
实时特征管线的多路事件收口
阶段:特征工程与在线数据管道
解决的问题:推荐系统在线特征要同时消费点击流、曝光流、画像更新流。传统方式为每路数据起一个 goroutine 各写各的,并发写同一特征 key 会互相覆盖,还得引入分布式锁。用 select 统一收口:多个上游 channel 汇入一个特征管理器 goroutine,串行处理,天然避免写冲突。
具体案例:某短视频推荐的实时特征服务,用 for-select 同时监听点击流 ch、曝光流 ch 和心跳 ticker(每 10 秒上报一次消费水位)。所有特征更新都在单一 goroutine 内完成,彻底消除了锁竞争,代码反而更简单。
分布式训练的心跳监控
阶段:训练任务生命周期管理
解决的问题:参数服务器要实时感知几十个 worker 是否存活。传统轮询每个 worker 的状态文件耗时且不实时;select + time.After 为每个 worker 分配一条心跳 channel,超过 30 秒未心跳就标记离线并触发补丁任务重调度。
具体案例:一个千卡集群的训练平台,用 select 在单 goroutine 内监听所有 worker 的心跳 channel,宕机发现时间从分钟级缩短到秒级。
Python 演示
import randomimport threadingimport timefrom queue import Queue, Empty class Channel: """模拟 Go 的 channel:有界队列 + 就绪探测""" def __init__(self, capacity=10): self.queue = Queue(maxsize=capacity) def send(self, item): self.queue.put(item) def recv_nowait(self): try: return self.queue.get_nowait() except Empty: raise BlockingIOError("channel 为空") def go_select(channels, timeout=None): """模拟 select 多路监听: 同时盯住多个 channel,有就绪的随机选一个,支持超时。 """ deadline = None if timeout is None else time.time() + timeout while True: ready = [i for i, ch in enumerate(channels) if not ch.queue.empty()] if ready: chosen = random.choice(ready) return chosen, channels[chosen].recv_nowait() if deadline is not None and time.time() >= deadline: return None, None time.sleep(0.005) def producer(ch, items, delay): for item in items: time.sleep(delay) ch.send(item) if __name__ == "__main__": ch_a = Channel() ch_b = Channel() ch_c = Channel() threads = [ threading.Thread(target=producer, args=(ch_a, ["A1", "A2", "A3"], 0.08), daemon=True), threading.Thread(target=producer, args=(ch_b, ["B1", "B2"], 0.15), daemon=True), threading.Thread(target=producer, args=(ch_c, ["C1"], 0.05), daemon=True), ] for t in threads: t.start() for _ in range(6): idx, item = go_select([ch_a, ch_b, ch_c], timeout=0.5) if idx is None: print("超时:没有消息到达") else: print("收到来自 ch{} 的消息: {}".format( chr(ord('A') + idx), item))
ready = [i for i, ch in enumerate(channels) if not ch.queue.empty()]扫描所有 channel,收集当前就绪的下标集合,相当于 select 的第一轮就绪扫描。chosen = random.choice(ready)从就绪集合中随机选一个,模拟 Go select 的随机公平策略。if deadline is not None and time.time() >= deadline:超时判断:到了截止时间还没有就绪 channel,就返回 None 结束监听。time.sleep(0.005)让出 CPU 避免忙等,模拟 goroutine 挂起等待的语义。threading.Thread(target=producer, args=(ch_a, ["A1", "A2", "A3"], 0.08), daemon=True)启动生产者 goroutine,每隔一定延迟向 channel 发送一条消息。idx, item = go_select([ch_a, ch_b, ch_c], timeout=0.5)主线程同时监听三个 channel,谁先就绪处理谁,整体超时 0.5 秒。
常见误区
误区“select 和 switch 一样,从上往下匹配呗?哪个 case 写在前面就执行哪个。我把重要的 case 放最上面不就行了?”
正解select 不会按书写顺序执行。当多个 case 同时就绪时,Go 会在运行时伪随机选择一个,保证公平。如果真有“顺序优先”语义,写在前面的 case 会一直抢占,后面的 case 形同虚设。随机性不是 bug,而是语言刻意设计的行为。回到值班经理的比喻:小李不会因为退换货窗口排在白板第一位,就永远优先处理它。
为何常见:select 的语法和 switch 太像了,而大多数编程语言的 switch/case 都是顺序匹配,初学者很容易把惯性思维套过来。
误区“select 加了 default 之后,每次都会走 default,case 里的数据永远收不到,所以非阻塞模式没啥用?”
正解恰恰相反。select 每轮会先检查所有 case 是否就绪,只要有一个 case 就绪,就优先执行就绪的 case;只有全部 case 都没就绪时,才走 default。default 是“兜底计划”,不是“抢占者”。就好比小李巡场时,如果扫到某个窗口正好有人按铃,他肯定先服务客户,没人了才继续巡场。
为何常见:把 default 理解成了 switch 里“无条件兜底”的分支,忽略了 select 的“就绪优先”语义。
误区“channel 是 nil 的时候,select 会不会直接 panic 崩溃?我都不敢往 select 里传可能为 nil 的 channel。”
正解不会 panic。nil channel 的收发操作永远无法就绪,select 会直接无视这个 case。如果其他 case 也没就绪且没有 default,select 依然阻塞。这个特性反而很有用:把某个 channel 变量置为 nil,就能在 select 中“动态禁用”它,而不必改写 case 列表。
为何常见:nil 在 Go 里通常意味着“危险的零值”——nil map 写入会 panic,nil 指针调用会崩溃,初学者容易把 nil channel 也想成异常,实际上 channel 的 nil 是被明确定义为“永不就绪”的。
误区“我在 select 的 case 里写 break,想退出整个 for-select 循环,但程序好像一直在跑,停不下来?”
正解case 里的 break 只会跳出 select 语句,跳出后 for 循环继续下一轮。要退出外层 for,必须使用 return,或者给 for 加标签后用带标签的 break。这就好比小李接到“下班”指令后,必须离开值班室(return),而不是仅仅挂断对讲机(break)——挂断了他还在屋里站着呢。
为何常见:break 在大多数语言里都是“退出最近的循环或 switch”,而 select 是一个长得像 switch 却又不是 switch 的语句,break 的靶子自然就错位了。
面试题
Q1. Go 的 select 在多个 case 同时就绪时会如何执行?
- A.按 case 从上到下的顺序执行
- B.伪随机选择其中一个 case 执行← 正确答案
- C.依次执行所有就绪的 case
- D.编译报错,不允许同时就绪
答案:B。select 在多个 case 同时就绪时会伪随机选一个,这是语言层面的公平设计。A 是 switch 的语义,C 会破坏“每次只处理一个事件”的模型,D 显然不对。就像小李不因为哪个窗口写在白板前面就偏心。
Q2. 关于 select 语句中 case 的操作类型,正确的是?
- A.只能包含 channel 的接收操作
- B.只能包含 channel 的发送操作
- C.可以包含发送接收或 default← 正确答案
- D.可包含任意表达式和函数调用
答案:C。select 的每个 case 必须是 channel 的发送、接收或 default,不能是任意表达式或函数调用。A 和 B 都只说了一半,D 违背了 select 的语法约束。这正是“值班白板上只能写窗口相关事件”的道理。
Q3. 当 select 的所有 case 都未就绪且存在 default 时,程序会?
- A.阻塞等待某个 case 就绪
- B.执行 default 分支后继续← 正确答案
- C.直接 panic 程序崩溃
- D.随机等待一段时间再检查
答案:B。default 就是“全部 case 未就绪”时的兜底执行分支,执行完 select 语句后程序继续向下走,不会阻塞。A 是没有 default 时的行为,C 和 D 与 select 语义完全不符。就像小李巡场时发现没人,就继续巡而不是原地站桩。
Q4. select 中的 nil channel 起到什么作用?
- A.会引发运行时 panic 错误
- B.会立即变成就绪状态
- C.相当于被禁用,永远无法就绪← 正确答案
- D.只有发送操作用 nil 时才生效
答案:C。nil channel 的收发操作永远不会就绪,select 会忽略它,既不会 panic 也不会被选中。这个特性常被用来动态屏蔽某个 channel。A 是 nil map 的行为,B 和 D 与 Go 运行时对 nil channel 的定义矛盾。
Q5. time.After 的返回值适合用于 select 的什么场景?
- A.当作一个只会触发一次的闹钟 case← 正确答案
- B.当作周期性触发的心跳信号
- C.当作同步等待多个协程的信号
- D.无法在 select 中使用 time.After
答案:A。time.After(d) 返回一个 channel,在 d 时长后收到一次数据,天然适合做一次性超时保护。周期性心跳应该用 time.Tick 或 time.NewTicker。C 说的是 WaitGroup 的活,D 明显错误。就像小李定一个 3 分钟闹钟,只响一次。
Q6. 关于 select 配合时间控制,正确的有?
- A.time.After 的 channel 会在到达时长后收到值← 正确答案
- B.time.Tick 会周期性向 channel 发送值← 正确答案
- C.for-select 中常配合 After 实现超时保护← 正确答案
- D.time.After 一旦调用就会阻塞当前 goroutine
答案:ABC。A 和 B 分别是 time.After 和 time.Tick 的基本行为,C 是标准超时模式。D 错误:time.After 只是创建 channel 和定时器,不会阻塞调用方 goroutine,阻塞的是 select 的监听行为。结合值班比喻:定闹钟的瞬间小李还在干活,只是闹钟到点会响。
Q7. 在 for-select 循环中,收到停止信号后需要马上退出整个循环,下列做法正确的是?
- A.在 case 中直接写 break 即可退出
- B.使用带标签的 break 或 return 退出← 正确答案
- C.使用 continue 跳到下一次循环
- D.让 select 自然结束循环即可
答案:B。case 里的 break 只能跳出 select 这一层,不能退出外层 for;必须用带标签的 break 或 return。C 的 continue 只会跳过后面的代码继续下一轮,D 中 select 没有“自然结束”的机制。就像小李收到下班指令后必须走出值班室(return),而不是只挂断对讲机(break)。
Q8. 以下哪种场景最适合使用 select + default 的非阻塞模式?
- A.必须等待数据库查询结果返回
- B.尝试写入日志通道满时直接丢弃← 正确答案
- C.等待退出信号优雅关闭服务
- D.在线程池中同步执行一个任务
答案:B。非阻塞模式适合“可做可不做、不做也不影响正确性”的附加操作,日志缓冲满了丢弃旧日志正是典型场景。A 和 C 语义上要求必须等到结果,应该用阻塞 receive。D 是同步执行,与 select 无关。就像小李巡场时顺手看窗口,没人在就继续巡。
Q9. 关于 reflect.Select 动态多路复用,正确的有?
- A.可在运行时动态构造 case 列表← 正确答案
- B.能监听动态增删的 channel 集合← 正确答案
- C.运行开销高于编译期静态 select← 正确答案
- D.不支持 default 分支配逻辑
答案:ABC。reflect.Select 用 reflect.SelectCase 切片在运行时构造 case,增删 channel 只需改切片,A、B 正确。反射机制有动态分派开销,C 正确。D 错误:reflect.SelectCase 的 Dir 字段支持 SelectDefault,可以表示 default 分支。
Q10. 关于 select 的底层调度机制,正确的有?
- A.编译后由 runtime.selectgo 统一调度← 正确答案
- B.就绪 case 随机选中以保证公平← 正确答案
- C.default 只在全部 case 未就绪时执行← 正确答案
- D.select 每次只能监听一个 channel
答案:ABC。select 会被编译为 runtime.selectgo 调用,它会打乱 case 顺序并随机选中就绪 case,A、B 正确。C 是 default 的明确定义。D 错误:select 的精髓恰恰是同时监听多个 channel。用值班经理的话说:调度员一次应对多个窗口,随机选一个,绝不偏袒。