7.8 KiB
tags, create time
| tags | 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(深入)— 场景推理
阅读下面代码,运行结果是什么?
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?
// 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 模式处理任务:
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 的知识,给出改进方案并说明至少三个关键点。
答题框架提示:
- 如何让每个 worker 感知到停止信号?
- context 和 channels 如何配合使用?
- 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:参考答案要点:
-
使用 context 传递取消信号:worker 内部不应只用
range,而应改用for-select模式,同时在 case 中监听ctx.Done(),这样 context 被取消时每个 worker 都能收到信号并返回退出。 -
增加一个 stopper channel:可以在 main/goroutine 中创建一个单独的 done channel,与 jobs channel 分开管理。当需要停止时,关闭 stopper channel,workers 检测到关闭后即退出。或者直接使用 ctx 的 Done channel。
-
正确组合 select 与 ctx.Done():每个 worker 的 for 循环改写为:
for {
select {
case job, ok := <-jobs:
if !ok {
return // jobs channel 被关闭
}
process(job)
case <-ctx.Done():
return // context 被取消
}
}
- 调用方确保 cancel 被调用:startWorkers 的调用者应在合适的时机(如 HTTP handler 返回时)调用
cancel(),并通过 defer 保证一定会执行。
评分标准:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。