想象你是一家大餐厅的传菜领班。顾客络绎不绝,你一声令下,后厨十几个档口同时开火——炒菜的、蒸鱼的、烤串的,热火朝天。可突然,几位客人等不及起身走了。问题来了:你得赶紧通知后厨“那桌的菜别做了”,但灶台忙成一锅粥,有些厨师根本不知道顾客走了,还在闷头炒一盘永远不会有人来吃的菜。灶台被占着,食材被浪费,新单子进来反而出不了餐——服务越来越慢,最后整个餐厅被拖垮。 Go 语言的 goroutine 就像后厨里的厨师,启动成本极低,go func() 一行就能让一个任务跑起来。启动容易,管住难。干活的 goroutine 一旦收不到“停手”的信号,就会一直占着内存和 CPU,像那个闷头炒菜的厨师。context 包就是 Go 官方给传菜间装的那套“大喇叭广播系统”:一个取消信号传下去,所有相关的 goroutine 收到通知,自动停手、收拾灶台、把占用的资源还回来。这套系统还内置超时机制——每道菜设定出餐时限,到点自动作废;即便有 goroutine 悄悄堆积,你也可以打开 pprof 这个“后厨监控”,逐一揪出是谁在闷头做没人要的菜。 这篇文章,我们就从这套“后厨大喇叭”讲起,彻底搞懂 Go context 的取消与超时。

岗位关联

掌握 context 等于掌握了 goroutine 生命周期管理的钥匙,这是初、中级 Go 开发者迈向高并发服务稳定性的分水岭技能。

目录
  1. 可视化演示
  2. Context 入门
  3. 取消传播机制
  4. 超时与期限
  5. 防泄漏机制
  6. 级联取消实战
  7. HTTP 传递
  8. pprof 排查
  9. 最佳实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Go context从Root向子goroutine(DB/API/File)及孙goroutine(G1-G4)的级联取消传播,Cancel和Timeout信号确保所有goroutine及时退出。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Context取消与超时 Root context创建子context(DB/API/File),每个子context可再派生孙goroutine(G1-G4) Cancel信号从Root向下级联传播,Timeout自动触发取消,防止goroutine泄漏 ctx, cancel := context.WithTimeout(parent, d)

Context 入门

餐厅传菜间最值钱的设备,不是菜刀也不是铁锅,而是那套挂在墙上的大喇叭。领班不需要跑遍每个档口,只要对着喇叭喊一声,炒锅前的师傅、烤箱旁的学徒、水池边的帮工全都听得见。context 在 Go 程序里扮演的正是这个角色——它是挂在每个任务身边的“信号喇叭”,专门用来传递一道指令:停手。

在 Go 里,任务通常是一个 goroutine——一个轻量级的并发执行单位。goroutine 启动只要几微秒,但想让它停下来,就得靠 context。context 是一个接口,官方定义了四个“喇叭功能”,任何实现它的类型都要提供这四种能力:

context 树必须有根。context.Background() 返回一个永不取消的空 context,是所有 context 树的起点;context.TODO() 是占位符,表示“暂时还不确定用什么 context,先占着”。“context 必须作为函数第一个参数显式传递”是 Go 社区的强制约定,就像每个档口的大喇叭都必须接到总线上,信号才能一路通到最角落的厨师耳边。

Context:⟨Deadline(),Done(),Err(),Value()⟩\text{Context} : \langle Deadline(), Done(), Err(), Value() \rangle
  1. Deadline(最后时限)返回该 context 的最后期限时刻;没有设置期限时 ok 为 false,goroutine 可用它判断任务还剩多少时间。
    Deadline()→(t,  ok)Deadline() \to (t,\; \text{ok})
  2. Done(取消信号)返回一个只读通道。context 被取消时通道关闭,goroutine 通过读取它判断“该不该停手”。
    Done()→channel (closed on cancel)Done() \to \text{channel (closed on cancel)}
  3. Err(取消原因)通道关闭后,用 Err 区分是被手动取消(Canceled)还是超时(DeadlineExceeded)。
    Err()→{Canceled,  DeadlineExceeded,  nil}Err() \to \{\text{Canceled},\; \text{DeadlineExceeded},\; nil\}
  4. Value(随单便签)读取 context 链路上携带的键值对,像外卖单上贴的备注。
    Value(key)→valueValue(key) \to value

取消传播机制

现在回到那个忙碌的夜晚。领班看到客人走了,走到传菜间,按下“全店广播”按钮。接下来的事情不需要挨个通知——信号顺着线路自动传到每一个档口的喇叭,所有厨师同时听到了“停手”。context 的取消传播,就是这套“按下按钮,全员皆知”的机制。

在 Go 里,按下按钮的动作是调用 cancel 函数。用 context.WithCancel(parent) 会返回一个新 context 和它的取消函数;一旦调用 cancel(),这个 context 以及它所有的后代 context,都会收到取消信号——它们的 Done() 通道同步关闭。

注意,取消不是强杀。context 不会把正在跑的 goroutine 一枪毙掉,它只是把“停手”的信号送到。每个 goroutine 必须自己主动检查——最常见的写法是 select 同时监听业务通道和 ctx.Done(),一旦 Done() 通道关闭就从业务循环里退出来。这叫做“协作式取消”,goroutine 放下手头工作,自己走人。就像厨师听到喇叭后自己关火,而不是领班跑进来夺刀。

cancel(p)⇒∀c∈children(p),  Done(c) 关闭\text{cancel}(p) \Rightarrow \forall c \in \text{children}(p),\; \text{Done}(c)\ \text{关闭}
  1. cancel(p)(按下广播按钮)调用根 context 的取消函数,激活传播。
    p.cancel()p.\text{cancel}()
  2. ∀c ∈ children(p)(广播覆盖所有档口)从 p 派生的全部子 context 都会被通知,无需逐个调用。
    ∀c∈children(p)\forall c \in \text{children}(p)
  3. Done(c) 关闭(信号抵达)每个子 context 的 Done 通道关闭,等待中的 goroutine 立即苏醒并退出。
    Done(c) closed\text{Done}(c)\ \text{closed}

超时与期限

连锁餐厅有个规矩:每道菜必须在限定时间内上桌,超时就作废重做。后厨的大喇叭系统也内置了定时器——领班不用手动盯着表,时间一到,喇叭自动响起,“这道菜取消!”

context 提供了两种定时器:WithTimeout 设定相对时长(比如 2 秒后取消),WithDeadline 设定绝对时间点(比如 17:30:00 到点取消)。它们返回的 context 会在指定的时间点自动触发取消,等价于 Go 内部帮你调用了一次 cancel。

实际上 WithTimeout 内部实现就是调用 WithDeadline(parent, time.Now().Add(timeout))。超时的意义在于给外部依赖设定红线:比如调用下游 API 最长等 3 秒,3 秒没响应就取消这次请求。此时 ctx.Err() 返回 DeadlineExceeded,你可以据此区分“真的超时了”还是“上游主动取消”。时限到了喇叭自动响,后厨不用人盯——这就是 context 超时机制的意义。

teffective=min⁡(tdeadline,  ttimeout)t_{\text{effective}} = \min(t_{\text{deadline}},\; t_{\text{timeout}})
  1. t_deadline(绝对期限)WithDeadline 设置的固定时刻,例如 17:30:00 整到点取消。
    tdeadline=某一天某一刻t_{\text{deadline}} = \text{某一天某一刻}
  2. t_timeout(相对超时)WithTimeout 以“当前时刻 + 时长”换算成内部的绝对时刻。
    ttimeout=tnow+dt_{\text{timeout}} = t_{\text{now}} + d
  3. min(先到先触发)两种期限同时存在时,谁更早触发谁生效,保证最晚不超过最早的期限。
    teffective=min⁡(tdeadline,ttimeout)t_{\text{effective}} = \min(t_{\text{deadline}}, t_{\text{timeout}})

防泄漏机制

顾客走了,厨师不知道,还在闷头炒一道永远不会被端上桌的菜。他占着灶台、耗着燃气、堆着食材。餐厅的灶台有限,炒废的菜越来越多,新客人的单反而做不出来了——这就是后厨版本的 goroutine 泄漏。

goroutine 泄漏的典型场景:一个 goroutine 阻塞在 channel 上等待消息,但发送方永远不会来了(因为请求早已结束、没人发数据),goroutine 就一直阻塞下去。泄漏的 goroutine 不会自己消失,它占着栈内存和调度器槽位,越积越多,最后服务内存暴涨、性能雪崩。

防泄漏的关键手段是 select + Done()。一个需要长时间运行的 goroutine,它的内部循环里必须监听 ctx.Done():业务通道收到数据就处理,Done 通道关闭就立刻 return。同时别忘了 defer 清理资源——比如关闭数据库连接、释放文件句柄。defer 保证即使 goroutine 中途退出,资源也会归还。每个 goroutine 都应该“耳听六路”——一边干活,一边听着大喇叭。听到“停手”就关火、收拾灶台、走人。

Nleak=Nstarted−NfinishedN_{\text{leak}} = N_{\text{started}} - N_{\text{finished}}
  1. N_started(启动总数)通过 go 关键字启动的 goroutine 数。启动廉价,但没人管就停不下来。
    Nstarted=∑go func()N_{\text{started}} = \sum \text{go func()}
  2. N_finished(退出总数)函数完整返回的 goroutine 数。只有 return 才算真正结束。
    Nfinished=∑returnN_{\text{finished}} = \sum \text{return}
  3. N_leak(差值)差值为正,说明存在永远卡死的 goroutine。
    Nleak=Nstarted−Nfinished>0N_{\text{leak}} = N_{\text{started}} - N_{\text{finished}} > 0

级联取消实战

一桌客人点了一桌菜:凉菜、热菜、汤。客人突然全走光了,领班按下广播,配菜间、炒锅、炖汤的瓦罐全部同时停手。这个取消不是孤立的——它是从“这一桌订单”这个根节点,沿着订单的每个子项传递下去,一道菜都不漏。

程序中往往不只是单个 goroutine,而是一棵由 context 构成的树:根 context 派生多个子 context,每个子 context 又派生孙 context。当根被取消,信号沿着树结构递归传导,所有层级的 goroutine 在几乎同一时刻收到通知。这就是级联取消。

级联取消还有一个重要课题:部分完成回滚。比如一个下单流程:扣库存 → 生成订单 → 调用支付。如果支付环节超时取消,前面的扣库存必须回滚。实际代码中,cancel 信号传回来时,每个环节都要用 select 监听 Done(),收到后执行自己的补偿逻辑(defer 或专门的回滚函数)。一次广播,整桌菜联动停手,还能顺手把已经做了一半的菜处理掉——这就是级联取消的威力。

ttotal=max⁡c∈activetcancel(c)t_{\text{total}} = \max_{c \in \text{active}} t_{\text{cancel}}^{(c)}
  1. active(仍在工作的子树)记录尚未响应取消的子孙 context 集合。
    c∈activec \in \text{active}
  2. t_cancel^(c)(单点停手耗时)每个子任务从收到信号到完成清理所需的时间。
    tcancel(c)t_{\text{cancel}}^{(c)}
  3. max(最慢者决定总量)整棵树的“完全停净”时间由最慢的子任务决定。
    ttotal=max⁡ctcancel(c)t_{\text{total}} = \max_{c} t_{\text{cancel}}^{(c)}

HTTP 传递

外卖平台上,顾客的订单条会跟着订单流程走:接单的商家看到一份,骑手看到一份,平台看到一份。顾客取消订单后,这条取消指令会同步到商家、骑手、平台所有环节。HTTP 请求之于 Go 服务,就是那条“订单条”,而 context 是附在订单条上的“信号线”。

在 Go 的 HTTP 服务里,服务端每个请求都会生成一个 context:r.Context(),它的生命周期跟请求绑定——客户端断开连接时,这个 context 会自动取消。服务端处理函数应该把 r.Context() 传给下游调用,这样客户端一断线,整条处理链立刻知道。

客户端方向,http.NewRequestWithContext(ctx, method, url, body) 可以把带超时的 context 注入请求。当 ctx 超时或取消时,HTTP 请求自动中断。这条 context 还能跨服务传递:服务 A 收到请求后,用同一个 ctx(或派生一个带更短超时的子 ctx)调用服务 B,B 收到请求时,取消信号天然穿透。链路追踪工具(如 OpenTelemetry)同样基于这个机制,把 trace id 放进 ctx 一路透传。

订单条走到哪儿,取消指令就跟到哪儿。HTTP 链上的每个服务,共享同一根“信号线”。

Trequest=tend−tstart≤TctxT_{\text{request}} = t_{\text{end}} - t_{\text{start}} \le T_{\text{ctx}}
  1. t_end - t_start(请求总耗时)服务从收到请求到处理完成的墙钟时间。
    T=tend−tstartT = t_{\text{end}} - t_{\text{start}}
  2. T_ctx(超时预算)调用链上约定的取消上限。
    Tctx=WithTimeout 的时长T_{\text{ctx}} = \text{WithTimeout 的时长}
  3. ≤(硬约束)请求必须在 ctx 期限内完成,超时就被取消。
    Trequest≤TctxT_{\text{request}} \le T_{\text{ctx}}

pprof 排查

餐厅经理发现最近的出餐效率越来越低,后厨明明很忙,菜却出不来。他打开监控回放,发现角落有个厨师面前堆了一摞炒好的菜,没有骑手来取,他却还在不断地炒。经理顺着监控,找到了这个“只干活不交付”的厨师。对应到 Go 服务,这个“监控”就是 pprof。

net/http/pprof 是 Go 标准库内置的性能剖析工具,引入后会自动注册一组 /debug/pprof/ 端点。其中 /debug/pprof/goroutine 会输出当前所有 goroutine 的堆栈快照。堆栈会显示每个 goroutine 阻塞在哪一行代码——是等 channel、等锁、还是 time.Sleep。堆积的 goroutine 会在同一行代码附近大量重复,一眼就能锁定泄漏点。

实际操作:先让服务出问题,然后运行 go tool pprof http://localhost:6060/debug/pprof/goroutine,或直接在浏览器访问。进入交互界面后 top 看哪类 goroutine 最多,list 进入具体函数看堆栈。修复思路通常是给那个永远等不到的 channel 加上 select + ctx.Done(),让 goroutine 有路可退。修完再采样,确认堆积数降下来。打开监控,找到那位闷头炒菜的厨师,给他接上大喇叭——goroutine 有了 Done() 通道,就不会再一直做没人要的菜。

Nstuck(loc)=∑i[Stacki=loc]N_{\text{stuck}}(\text{loc}) = \sum_{i} [\text{Stack}_i = \text{loc}]
  1. Stack_i(第 i 个 goroutine 堆栈)pprof 抓取 goroutine 时记录其阻塞位置。
    Stacki\text{Stack}_i
  2. [···](艾弗森括号)条件成立记 1、不成立记 0,用来做计数。
    [Stacki=loc][\text{Stack}_i = \text{loc}]
  3. Σ(汇总堆叠数)同一位置出现的次数越多越可疑。
    Nstuck=∑i[Stacki=loc]N_{\text{stuck}} = \sum_i [\text{Stack}_i = \text{loc}]

最佳实践

一家运转良好的餐厅,一定有一张贴在墙上的操作守则:大喇叭保持畅通、每道菜标注时限、听到广播立即停手。Go 社区对 context 的使用,也沉淀出了一套纪律。守住纪律,服务轻装上阵;打破纪律,goroutine 堆积成灾。

四条铁律:第一,context 必须作为函数的第一个参数传入——ctx 放在最前面,显式传递,禁止藏在 struct 里;第二,不要忽略 cancel 函数——WithCancel/WithTimeout 返回的 cancel 必须调用,通常紧跟 defer cancel();第三,context 不该存进 struct,也不该存到全局变量;第四,WithValue 只携带请求级元数据(trace id、用户身份),不要塞数据库连接等重资源。

还有一个常见约束:不要滥用 WithValue。context 的 Value 更像“请求作用域元数据”,不是“依赖注入容器”。每次 WithCancel 派生出的 cancel 都要调用,否则父 context 的取消信号会被“按住”,子树无法及时释放——编译器帮不了你,只能靠纪律。守则贴在墙上,大喇叭每天试音,后厨就永远井井有条。Go 服务也一样——把 context 纪律内化成习惯,goroutine 就会乖乖地来、乖乖地走。

P(无泄漏)=P(传递)×P(cancel)×P(清理)P(\text{无泄漏}) = P(\text{传递}) \times P(\text{cancel}) \times P(\text{清理})
  1. P(传递)(context 进首参)调用链每一层都把 ctx 传下去,信号才能到达每一个 goroutine。
    P(传递)P(\text{传递})
  2. P(cancel)(取消函数被调用)cancel 被 defer 调用,即使函数提前 return 也保证执行。
    P(cancel)P(\text{cancel})
  3. P(清理)(资源被释放)defer 里关闭连接、通道、文件句柄。
    P(清理)P(\text{清理})

AI 应用场景

大模型推理的请求取消

阶段:模型服务化部署与在线推理

解决的问题:大模型推理的 token 生成通常要 10~60 秒,用户中途关掉页面或点击“停止生成”,如果服务端不感知,后端的生成循环和上游检索协程会继续跑完整个序列,白白消耗 GPU 算力。用 context 就能在客户端断连的瞬间级联取消。

具体案例:某 RAG 助手服务,一次回答要并行启动“向量检索”“文档重排”“LLM 流式生成”三个 goroutine。入口通过 r.Context() 拿到请求 context,再 WithTimeout 派生 15 秒超时上下文传给三个子任务。用户一断线,Done() 通道关闭,三个 goroutine 立即退出,GPU 显存快速释放,服务吞吐不降。

训练数据管道的超时控制

阶段:数据预处理与特征工程

解决的问题:训练前的数据管道要拉取海量网页、图片、文本。某个下游对象存储偶尔变慢,worker 一旦卡死,整条 pipeline 的 goroutine 越堆越多。用 context.WithTimeout 给每个拉取请求设红线,超时自动取消并跳过。

具体案例:数据处理 pipeline 开了 50 个 worker goroutine 并发抓取 10 万条样本。每个 worker 的拉取请求用 context.WithTimeout(3s) 控制,超时自动取消并记一条失败日志;主 pipeline 的根 context 一取消,50 个 worker 全部收手。处理速度稳定,再也不会半夜卡死。

多 Agent 编排的级联取消

阶段:Agent 任务编排与工具调用

解决的问题:多 Agent 协作时,主任务一旦废弃,子 Agent 还在继续调用模型、消耗 token 预算。用 context 做级联取消,能让所有子 Agent 在几百毫秒内收手。

具体案例:一个“行业调研 Agent”会分解出“搜索摘要 Agent”“数据分析 Agent”“报告撰写 Agent”。编排器为每个子 Agent 派生独立 context;用户取消主任务时,编排器 cancel 根 context,所有子 Agent 的模型调用循环检查 Done(),及时中断并释放并发占用的连接。

Python 演示

python53 行
import threadingimport timefrom dataclasses import dataclass, field  @dataclassclass SimContext:    parent: 'SimContext' = None    cancelled: bool = False    children: list = field(default_factory=list)    _done: threading.Event = field(default_factory=threading.Event)     def is_cancelled(self):        if self.parent and self.parent.is_cancelled():            return True        return self.cancelled     def cancel(self):        if self.cancelled:            return        self.cancelled = True        self._done.set()        for child in self.children:            child.cancel()     def with_timeout(self, seconds):        child = SimContext(parent=self)        self.children.append(child)        threading.Timer(seconds, child.cancel).start()        return child     def spawn_work(self, name, step_wait):        def worker():            for i in range(5):                if self.is_cancelled():                    print(f"[{name}] 收到取消信号, 停止工作")                    return                print(f"[{name}] 步骤 {i + 1}/5 进行中")                time.sleep(step_wait)            print(f"[{name}] 正常完成")        threading.Thread(target=worker, daemon=True).start()  if __name__ == "__main__":    root = SimContext()    order = root.with_timeout(2.0)    order.spawn_work("洗菜师傅", 0.4)    order.spawn_work("掌勺师傅", 0.3)    time.sleep(1.0)    print("主流程: 客户取消订单, 广播取消信号")    root.cancel()    time.sleep(0.5)    print("主流程结束")
目的:用 Python 的线程与事件机制,模拟 Go context 的取消传播、超时触发与协作式退出。
  1. child = SimContext(parent=self)每一只“子喇叭”都挂到父喇叭之下,形成 context 树。
  2. threading.Timer(seconds, child.cancel).start()模拟 WithTimeout:到点自动触发取消。
  3. if self.parent and self.parent.is_cancelled():逐级向祖先查询取消状态,模拟 Done 通道沿链传播。
  4. for child in self.children: child.cancel()cancel 函数级联通知全部子 context。
  5. if self.is_cancelled():worker 循环中的“协作式取消”判断,收到信号就收手。
  6. daemon=True守护线程不会阻止进程退出,但这也正是“无人管束的 goroutine 会一直占着资源”的写照。

常见误区

误区我以为调用 context 的 cancel 函数就能直接杀掉 goroutine,就像 kill 掉一个线程。反正 cancel 一调用,goroutine 立刻消失。

正解context 没有任何“强杀”能力。cancel 只是关闭 Done 通道,等 goroutine 自己来读。如果 goroutine 里没有 select 监听 Done(),或者它阻塞在一个永远不会返回的 channel 发送上,取消信号对它形同虚设。这就像餐厅大喇叭响了,但厨师正戴着降噪耳机,照样闷头炒菜。“协作式取消”要求每个 goroutine 主动配合。

为何常见:很多人把 context 类比成线程的 stop/kill,忽略了 Go 的并发哲学是“不要通过共享内存来通信,而要通过通信来共享内存”。

误区goroutine 自然跑完就结束了,cancel 函数不调用也没关系。我见过很多例子,没调 cancel 程序也正常跑。

正解WithCancel/WithTimeout 返回的 cancel 必须调用,官方文档明说“不调用会泄漏资源”。cancel 不调用,父子 context 的订阅关系不会被解除,内部的定时器和相关结构一直存活,直到整棵 context 树结束。正确姿势是紧跟 defer cancel(),保证任何路径返回都会触发。

为何常见:短小的示例程序里 goroutine 确实能正常跑完,看起来“没调用也没事”,掩盖了长期运行下的隐患。

误区我已经用了 context,goroutine 就应该自动收到取消。启动 goroutine 时把 ctx 传进去就行。

正解用 context.Background() 当根,然后启动 goroutine,等于只有一根哑线——Background 不会取消,也不携带任何信号。必须从 Background 派生 WithCancel/WithTimeout,并把子 context 显式传进 goroutine,取消机制才真正存在。直接用 Background 干活等于没装大喇叭,goroutine 永远收不到通知。

为何常见:新手容易把“用了 context 包”和“有了取消能力”画等号,忽略了派生和传递这两步关键动作。

误区把 context 放进 struct 里存着,调用方法时用字段访问,不是更省事吗?Java 里不都这么干。

正解Go 官方明确要求 context 作为函数第一参数显式传递,因为它的生命周期属于请求,而 struct 实例可能活得比请求长。把 ctx 存进 struct,一旦请求结束 ctx 被取消,后续再用这个对象时拿到的就是一根“过期信号线”。大喇叭应该跟着订单走,而不是钉在某个厨师的工位上。

为何常见:其他语言常把请求上下文存在对象或 ThreadLocal 里,换到 Go 容易按惯性照搬。

误区WithValue 能存任意数据,用它传参数最方便。什么都可以往 Value 里塞,函数签名还能干净很多。

正解WithValue 的 key 建议用自定义类型而不是内置 string,value 应当只放请求级元数据(trace id、用户 ID)。放数据库连接、大对象等重资源既违背官方语义,也让 key 管理混乱。外卖单上的备注栏写“不要香菜”可以,写整篇菜谱就乱了。

为何常见:value 的隐式传递看起来“零成本”,容易让人把它当全局 map 用,忽视了它本质上是请求作用域的元数据载体。

面试题

Q1. context.Background() 的作用是什么?

单选题 · easy

  • A.创建永不取消的根 context← 正确答案
  • B.创建带超时的根节点
  • C.创建可取消的 context 根
  • D.创建携带数据的 context

答案:A。Background 返回一个非 nil 的空 context,没有 deadline、不会取消、没有 value,是 context 树的根。它就像大喇叭系统的总电源——常开,但本身不发声。所有可取消的 context 都要从它派生,程序的主函数、init 函数或测试入口用 Background 作为起点。

Q2. 下列哪个方法用于设置相对超时?

单选题 · easy

  • A.WithTimeout 设置相对超时← 正确答案
  • B.WithDeadline 设置绝对时限
  • C.WithCancel 派生取消信号
  • D.WithValue 携带键值数据

答案:A。WithTimeout(parent, d) 接受相对时长 d,时间到自动取消。WithDeadline 接受绝对时刻;WithCancel 只提供手动取消;WithValue 只携带键值对。超时翻译成大白话:出餐时限从按下计时器那一刻开始算。

Q3. context 的 Done() 通道何时关闭?

单选题 · mid

  • A.goroutine 完成任务时自动关闭
  • B.context 被 GC 回收时关闭
  • C.context 被取消或超时时关闭← 正确答案
  • D.传入的值被修改时关闭

答案:C。Done() 返回一个只读通道,当 context 被取消(手动 cancel)或到达 deadline/timeout 时,运行时关闭这个通道。关闭后阻塞读取立刻返回,goroutine 借它感知“该停手了”。goroutine 完成自身工作和通道关闭无关,GC 和值修改也不会影响 Done。

Q4. 以下哪项是协作式取消的正确理解?

单选题 · mid

  • A.goroutine 被运行时强制终止
  • B.goroutine 主动检查 Done 并退出← 正确答案
  • C.context 自动回收 goroutine
  • D.等待 goroutine 自然结束

答案:B。协作式取消是 goroutine 主动检查 ctx.Done(),通常写在 select 分支里:业务数据来了处理业务,Done() 关闭了立刻 return。Go 不会强杀 goroutine,所以这种主动检查是取消生效的唯一途径。大喇叭响了,厨师必须自己关火。

Q5. 调用 cancel 后,子 context 会发生什么?

单选题 · mid

  • A.只有父 context 自己取消
  • B.子 context 继续正常运行
  • C.子 context 变成 Background
  • D.所有子 context 级联取消← 正确答案

答案:D。context 的取消是树形传播的:调用 cancel,当前 context 及其所有后代 context 的 Done 通道都会关闭。子 context 不用各自调用 cancel,只要祖先链条上有人取消,它就被级联取消。这正是“一次广播,全员皆知”。

Q6. 下列哪种场景最容易造成 goroutine 泄漏?

单选题 · hard

  • A.goroutine 阻塞写 channel 无接收方← 正确答案
  • B.goroutine 正常执行完返回
  • C.goroutine 用 select 监听 Done
  • D.goroutine 用 defer 释放资源

答案:A。无缓冲 channel 的发送或接收永远找不到对方、没有超时、也没人取消,goroutine 就永久阻塞,这就是典型的泄漏。正常返回、select 监听 Done、defer 释放资源都是正确做法,不会泄漏。厨师要一直做没人取的菜,灶台才会被越占越满。

Q7. HTTP 服务端如何获取当前请求的 context?

单选题 · mid

  • A.http.NewRequest 中创建
  • B.r.Context() 方法获取← 正确答案
  • C.全局 Background 直接使用
  • D.r.Body 读取请求体

答案:B。标准库 http 包会把每个请求的 context 挂在 *http.Request 上,服务端处理函数用 r.Context() 拿到它。它和请求生命周期绑定,客户端断开连接自动取消。http.NewRequest 是构造请求对象,r.Body 是请求体,都不是 context 来源。

Q8. 以下哪些是 context 的正确使用方式?

多选题 · mid

  • A.作为函数第一个参数传递← 正确答案
  • B.将 context 存结构体字段中
  • C.defer 调用取消函数← 正确答案
  • D.WithValue 塞大量业务数据

答案:AC。context 作为函数第一个参数是 Go 社区铁律;defer cancel() 确保取消函数被调用;存 struct 违反官方建议;WithValue 只适合少量请求级元数据,不适合塞大量业务数据。大喇叭要挂在请求上跟着走,而不是焊死在某个物件里。

Q9. 使用 pprof 排查 goroutine 堆积的正确步骤包括哪些?

多选题 · hard

  • A.引入 net/http/pprof 包← 正确答案
  • B.访问 goroutine 采样端点← 正确答案
  • C.分析堆栈定位阻塞点← 正确答案
  • D.直接重启服务器解决

答案:ABC。导入 net/http/pprof 并启动 HTTP 服务后,访问 /debug/pprof/goroutine 抓取所有 goroutine 堆栈,通过 top/list 分析哪些位置堆积最多,修复后重新采样验证。重启服务器只是治标,不能定位根因,就像把监控录像删了而不是去找那位闷头炒菜的厨师。

Q10. 关于 context 超时与取消,以下说法正确的有哪些?

多选题 · hard

  • A.WithDeadline 设置绝对时间点← 正确答案
  • B.父取消会级联取消子节点← 正确答案
  • C.context 能强制终止 goroutine
  • D.超时时间越大任务越安全

答案:AB。WithDeadline 设置绝对时间点(如 17:30:00),父 context 取消时子 context 级联取消是内置语义。context 没有强杀 goroutine 的能力,只能发信号;超时设得很大反而增加资源占用和拖慢故障响应,不是“越大越安全”。