Files

9.6 KiB
Raw Permalink Blame History

tags, create time
tags create time
test/review
go
context
cancellation
goroutine-lifecycle
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 函数的输出结果是什么?

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. 打印 "" 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 并加超时:

    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(各自可能有不同的超时):

    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 个要点即可得满分;完全正确需覆盖全部要点。

关联笔记