--- tags: [test/review, go, select, poll-multiplexing, fairness] create time: 2026-08-09 12:00 --- # Select 多路复用机制_测试题 ## 概述 本试卷覆盖 Go select 关键字的底层实现、随机公平性保证、nil channel 行为、default 分支语义、goroutine leak 场景及与 context 的组合用法,共 10 道题(6 选择 + 3 填空 + 1 简答)。 --- ## 一、选择题(6道,由浅入深) > **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景 ### Q1(基础)— 考察定义层面 select 关键字在 Go 中能处理以下哪种操作? A. 任意两个变量的比较运算 B. 多个 channel 的发送或接收操作 C. 多个 goroutine 的并行启动 D. 对 sync.Mutex 的加锁和解锁 ### Q2(基础)— 考察行为判断 当 select 中有多个 case 同时满足条件时,Go 会如何处理? A. 从上到下按顺序匹配第一个符合条件的 case B. 从所有可匹配的 case 中随机选择一个 C. 优先选择 buffer 较大的 channel 对应的 case D. 抛出一个 ambiguity error 让开发者修改代码 ### Q3(进阶)— 考察原理理解 关于 nil channel 在 select 中的行为,以下描述正确的是? A. 会导致 panic,程序崩溃 B. 该 case 永远不会被选中,类似于被永久禁用 C. 该 case 会被选中一次然后报错退出 D. 编译阶段就会报错,不允许写出这样的代码 ### Q4(进阶)— 比较辨析 以下哪个选项**等价于**给 select 添加 default 分支的效果? A. `case <-time.After(1 * time.Hour)` — 设置一个很长的超时 B. `case <-time.After(0)` — 设置超时时间为零 C. 将所有的 channel 改为有缓冲 channel D. 使用 `for` 循环包裹 select 反复执行 ### Q5(深入)— 场景推理 阅读下面代码,运行结果是什么? ```go var ch chan int = nil select { case <-ch: fmt.Println("received") default: fmt.Println("default") } ``` A. 打印 "received" B. 打印 "default" C. 永久阻塞 D. 触发 panic ### Q6(深入)— 源码级/边界场景 以下哪种写法**不会**导致 goroutine leak? ```go // A func a() { for { select { case v := <-ch: process(v) } } } // B func b(ctx context.Context, ch <-chan int) { for { select { case v, ok := <-ch: if !ok { return } process(v) case <-ctx.Done(): return } } } // C func c() { timer := time.NewTimer(5 * time.Minute) // 没有调用 timer.Stop(),timer goroutine 持续运行 } // D func d() { for { select { case <-time.After(1 * time.Second): doSomething() } } } ``` A. A B. B C. C D. D --- ## 二、填空题(3道) ### F1 — select 内部结构 每个 select 语句在编译时被转换为一个 `selstruct` 结构体,其中 `[填空1]` 字段存储 case 关联的 channel 指针数组,`order` 字段存储随机化后的访问顺序。 > **提示**: 这个字段名是英文复数形式,对应 "channels" 的缩写。 ### F2 — 随机性函数 Go 调度器中用于处理 select 多路复用的关键 netpoll 函数包括:`pollSurprise`(已注册 goroutine 突然可唤醒)、`pollRandom`(多选一时随机选择保证 `[填空1]`),以及 `pollDelay`(无 case 就绪时进入睡眠)。 > **提示**: pollRandom 的核心设计目的是防止某些 case 被系统性忽略。 ### F3 — 性能对比 在有延迟的场景下,应当优先使用 `default` 分支而非 `time.After(0)`,因为 time.After() 背后会创建一个 `[填空1]`,即使是 0 延迟也有额外开销。 > **提示**: time.After 内部使用了定时器相关的系统资源。 --- ## 三、简答题(1道) ### S1 某微服务中使用如下 Worker Pool 模式处理任务: ```go func startWorkers(ctx context.Context, jobs <-chan Job) { for i := 0; i < 10; i++ { go func(id int) { for job := range jobs { process(job) } }(i) } } ``` 该函数的调用者负责发送 job 到 jobs channel,但从未关闭过 channel。现在需要为这个 worker pool 增加优雅的停止能力。 请结合 select、context 和 channel 的知识,给出改进方案并说明至少三个关键点。 > **答题框架提示**: > 1. 如何让每个 worker 感知到停止信号? > 2. context 和 channels 如何配合使用? > 3. stopper 通道的角色是什么? --- ## 参考答案与解析 ### 选择题答案 | 题号 | 正确答案 | 解析 | |------|---------|------| | Q1 | B | select 只能用于 channel 相关操作(发送或接收)。A 应该用 if/else;C 用 `go` 关键字;D 用 Lock/Unlock。 | | Q2 | B | select 的核心设计之一就是随机选择——当多个 case 同时就绪时,通过 pollRandom 随机选一个,避免优先级饥饿问题。如果没有随机性,总是固定从某个 case 开始匹配会导致系统性忽略其他 case。 | | Q3 | B | nil channel 的 send/receive 本身会导致死锁(不是 panic),但在 select 中只是简单地不被选中,相当于永久禁用该 case。这不是编译错误,常被用于动态启用/禁用 case。 | | Q4 | B | `time.After(0)` 会在 0 秒后立即触发定时器,等效于 default 的非阻塞语义。但 A 太长时间无效;C 不改变 select 的行为特性;D 反而会导致无限循环的空转。 | | Q5 | B | nil channel 在 select 中永不匹配,所以 `<-ch` 不会被选中。由于存在 default 分支,立即执行 default,打印 "default"。这展示了 nil channel 作为 case 开关的用法。 | | Q6 | B | A 无限循环且无退出路径,会 leak;C 未 Stop timer,Timer goroutine leak;D 每次迭代创建新的 Timer goroutine,永不停止则无限泄漏。只有 B 同时检查了 channel close(ok == false)和 ctx.Done(),有明确退出路径。 | ### 填空题答案 | 题号 | 答案 | 解析 | |------|------|------| | F1 | `chans` | selstruct 中包含 chans([]*hchan,case 关联的 channel 数组)、recv/send(收发地址数组)、order(随机化顺序)、ncases(case 数量)。 | | F2 | `公平性` | pollRandom 的设计目的是保证多个可匹配 case 之间的公平性,防止某些 case 被系统性忽略(starvation)。面试中经常考"如果 select 按固定顺序匹配会发生什么"。 | | F3 | `timer goroutine` | time.After() 内部创建了一个定时器 goroutine 来等待指定时长后发送值。即使超时时间为 0,这个 goroutine 也会被创建,产生不必要的开销。无延迟场景应直接 prefer default。 | ### 简答题参考答案 S1:**参考答案要点**: 1. **使用 context 传递取消信号**:worker 内部不应只用 `range`,而应改用 `for-select` 模式,同时在 case 中监听 `ctx.Done()`,这样 context 被取消时每个 worker 都能收到信号并返回退出。 2. **增加一个 stopper channel**:可以在 main/goroutine 中创建一个单独的 done channel,与 jobs channel 分开管理。当需要停止时,关闭 stopper channel,workers 检测到关闭后即退出。或者直接使用 ctx 的 Done channel。 3. **正确组合 select 与 ctx.Done()**:每个 worker 的 for 循环改写为: ```go for { select { case job, ok := <-jobs: if !ok { return // jobs channel 被关闭 } process(job) case <-ctx.Done(): return // context 被取消 } } ``` 4. **调用方确保 cancel 被调用**:startWorkers 的调用者应在合适的时机(如 HTTP handler 返回时)调用 `cancel()`,并通过 defer 保证一定会执行。 **评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。 ## 关联笔记 - [[00.Go/concurrency/Select 多路复用机制]]