Files
autumn-recruitment/00.Go/concurrency/Select 多路复用机制_test.md
T

227 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 多路复用机制]]