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

7.8 KiB
Raw Permalink Blame History

tags, create time
tags create time
test/review
go
select
poll-multiplexing
fairness
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 的知识,给出改进方案并说明至少三个关键点。

答题框架提示:

  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 循环改写为:

for {
    select {
    case job, ok := <-jobs:
        if !ok {
            return // jobs channel 被关闭
        }
        process(job)
    case <-ctx.Done():
        return // context 被取消
    }
}
  1. 调用方确保 cancel 被调用:startWorkers 的调用者应在合适的时机(如 HTTP handler 返回时)调用 cancel(),并通过 defer 保证一定会执行。

评分标准:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。

关联笔记