Files

203 lines
9.6 KiB
Markdown
Raw Permalink 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, context, cancellation, goroutine-lifecycle]
create time: 2026-08-09 12:00
---
# Context 包详解_测试题
## 概述
本试卷覆盖 Context 接口定义、WithCancel/Timeout/Deadline/WithValue 四种 WithXXX 函数、链式传播机制、WithValue 性能陷阱及最佳实践,共 10 道题(6 选择 + 3 填空 + 1 简答)。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
Go Context 接口中,哪个方法用于获取取消信号?
A. `Deadline()` — 返回取消截止时间
B. `Done()` — 返回一个只读 channel,ctx 被取消时关闭
C. `Err()` — 返回取消原因错误
D. `Value(key)` — 键值对查询
### Q2(基础)— 考察行为判断
以下关于 `context.WithTimeout` 的描述,哪一个是**正确**的?
A. WithTimeout 与 WithCancel 功能完全相同,没有任何区别
B. WithTimeout 内部组合了 WithCancel 和一个 timer,会额外启动一个 goroutine
C. WithTimeout 不会创建新的 goroutine,所有等待都在用户态完成
D. WithTimeout 设置的超时时间到达后,子 ctx 自动 cancel 但父 ctx 不会受影响
### Q3(进阶)— 考察原理理解
关于 Context 的链式传播机制,当父 ctx 被 cancel 时会发生什么?
A. 只有直接监听该 ctx.Done() 的 goroutine 收到信号
B. cancel 信号从父到子递归传播,所有后代 ctx 都会被取消
C. 只有设置了 deadline 的子 ctx 会被取消,没有 deadline 的不受影响
D. 需要手动在代码中遍历 children map 逐个取消
### Q4(进阶)— 比较辨析
以下哪个场景**最适合**使用 `context.WithValue`?
A. 在深层嵌套的业务逻辑中传递用户偏好设置
B. 在 HTTP handler 中间件中携带 trace ID 供下游日志使用
C. 将数据库连接对象存入 context 跨层传递
D. 用 context 替代函数参数传递频繁的计数器和状态变量
### Q5(深入)— 场景推理
阅读下面代码,请问 main 函数的输出结果是什么?
```go
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 注意这行注释掉了的情况
select {
case <-time.After(1 * time.Second):
fmt.Println("done")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
```
A. 打印 "done"
B. 打印 "<nil>"
C. 打印 "context deadline exceeded"
D. 永久阻塞
### Q6(深入)— 源码级/边界场景
关于 context.Value 的性能特征,以下说法哪个是**错误**的?
A. Value() 查找沿链路逐层查找匹配的 key,时间复杂度为 O(depth)
B. 每次调用 WithValue 都会分配一个新的 context 对象
C. 如果 key 是 slice/map/function 类型,会导致运行时 panic
D. WithValue 会修改已有节点并原地更新 key-value 对以节省内存
---
## 二、填空题(3道)
### F1 — WithTimeout 底层实现
`context.WithTimeout(parent, timeout)` 的内部实现等价于 `context.WithDeadline(parent, time.Now().Add(timeout))`。两者底层使用同一个 `[填空1]` 结构体,它嵌入了 `*cancelCtx` 并持有一个 `time.Timer` 来触发定时取消。这意味着 WithTimeout 比 WithCancel 多了一个 `[填空2]` 开销。
> **提示**: 结构体名称是 Timer + Context 的组合词;额外的开销来源于定时器的调度机制。
### F2 — cancelCtx 数据结构
cancelCtx 是 Context 链式结构中最重要的节点类型,其关键字段包括:`mu sync.Mutex` 保护并发安全、`done chan struct{}` 用于发送取消信号、`children map[canceler]*cancelCtx` 追踪直接子节点集合、`err error` 存储取消原因、以及 `[填空1]` 布尔标志位表示是否已清理完子节点。
> **提示**: 这个字段名暗示了操作状态,防止重复清理子节点导致的问题。
### F3 — WithValue 的 key 最佳实践
使用 context.WithValue 时,key 必须是可比较的类型(支持 == 比较)。最佳实践是使用自定义不可导出的类型作为 key,例如定义 `type userIDKey struct{}`,这样不仅可以避免与其他包的 key 冲突,还可以利用 Go 的类型系统防止外部代码误用相同的 key。取值时需要配合类型断言:`id, ok := ctx.Value(userIDKey{}).([填空1])`。
> **提示**: 注意 Value 返回的是 any 类型,取出来需要 type assertion。
---
## 三、简答题(1道)
### S1
一个 Web API 处理流程如下:
1. HTTP handler 接收到请求
2. 先做鉴权,获取用户信息
3. 根据用户信息从数据库查订单列表
4. 同时从 Redis 查缓存,取最新商品推荐
5. 将以上三个结果合并后返回
每个步骤都需要有超时控制和完整的取消链路。请设计一套基于 Context 的方案,说明如何管理这个三层级的 goroutine 树(handler → worker goroutine → sub-task goroutine),确保任意一个环节失败或超时时整个请求能正确退出。
> **答题框架提示**:
> 1. 顶层 ctx 从哪里来?如何加超时?
> 2. 如何优雅地并行执行 DB 查询和缓存读取?
> 3. cancel 的释放时机和位置在哪里?
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | Done() 返回只读 channel,ctx 被取消时该 channel 关闭,所有监听者同时收到信号。A 只返回时间不产生信号;C 只返回取消原因字符串;D 用于附加值传递。 |
| Q2 | B | WithTimeout 内部调用 WithDeadline(now + timeout),创建一个 timerCtx 包含 timer 和一个后台 goroutine 等待超时触发 cancel。所以比 WithCancel 多了一个 goroutine 开销。D 的描述部分正确但不够完整——超时后是当前 ctx 被取消,不是父 ctx。 |
| Q3 | B | Context 采用树形嵌套结构,每个 cancelCtx 维护 children map。当父 ctx 被取消时,遍历 children 逐个取消,递归地向后代传播。这是一个自顶向下的 cascade 过程。 |
| Q4 | B | A 太深的嵌套不适合用 WithValue(性能差且 GC 压力大);C 明确违反"不用 context 传持久化资源"的原则;D 也是滥用 context。B 是标准用法——在中间件层加入跨层元数据(如 trace ID),通过 r.WithContext(ctx) 传入 handler 链。 |
| Q5 | A | time.After(1s) 比 WithTimeout(2s) 更快触发,所以 1 秒后 select 匹配到 time.After 的 case,打印 "done"。虽然 ctx 最终也会在 2 秒后被自动取消,但 select 已经在前一步完成了匹配。如果注释掉 defer cancel(),这里的行为不变——cancel 只是释放资源,不影响已完成的 select。 |
| Q6 | D | WithValue 不会修改已有节点,而是创建整个链路的新副本路径(每条边可能都要新建节点),N 次调用产生 N 个对象。A/B/C 均正确描述了 WithValue 的特性。D 恰恰相反——它不原地修改,而是分配新对象。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | `timerCtx`,`goroutine` | timerCtx 结构体包含 embed 的 *cancelCtx 和一个 time.Timer。底层由一个单独的 goroutine 等待 timer 触发后调用 cancel,因此比纯 WithCancel 多了一个 goroutine 开销。 |
| F2 | `childCleared` | childCleared 标记防止重复清理子节点。一旦子节点被清理完毕,该标志设为 true,后续即使再次触发取消也可以跳过遍历 children map 的步骤。 |
| F3 | `string` (或更通用的 `[类型]`) | ctx.Value() 返回 any 类型,取出时必须进行类型断言。示例中使用 string 是因为 value 本身是字符串类型。关键在于 key 用自定义私有类型而非公开类型,避免命名冲突。 |
### 简答题参考答案
S1:**参考答案要点**:
1. **顶层 ctx 继承 r.Context 并加超时**:
```go
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel() // handler 返回前必须释放
// ...
}
```
继承 `r.Context()` 保证上游 middleware(如 request-ID 注入)不被打断。
2. **并行执行 DB 查询和缓存读取**:用两个独立的 goroutine + WaitGroup,每个 sub-task 使用衍生自顶层 ctx 的子 ctx(各自可能有不同的超时):
```go
var wg sync.WaitGroup
ordersCh := make(chan []Order, 1)
recommendationsCh := make(chan []Product, 1)
wg.Add(2)
go func() {
defer wg.Done()
subCtx, sc := context.WithTimeout(ctx, 500*time.Millisecond)
defer sc()
orders, _ := db.QueryOrders(subCtx, userID)
ordersCh <- orders
}()
go func() {
defer wg.Done()
subCtx, sc := context.WithTimeout(ctx, 200*time.Millisecond)
defer sc()
recs, _ := redis.GetRecommendations(subCtx, userID)
recommendationsCh <- recs
}()
wg.Wait() // 等两个并行任务完成
```
每个 sub-task 用独立 Timeout 控制不同 SLA,互不影响。
3. **cancel 的释放时机**:
- handler 层的 `defer cancel()` 确保响应返回或发生 panic 时都能释放顶层 ctx 的资源(包括关联的 timer goroutine)
- 各 sub-task 内部的 `defer sc()` 确保各自的定时器也会被停止
- 任何环节检测到 `ctx.Err() != nil` 应立即终止,不再继续后面的操作
4. **关键原则回顾**:always pass ctx as first param、always defer cancel、never store ctx in structs、use context for cancellation not data transfer。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
## 关联笔记
- [[00.Go/concurrency/Context 包详解]]