想象你是一家大餐厅的传菜领班。顾客络绎不绝,你一声令下,后厨十几个档口同时开火——炒菜的、蒸鱼的、烤串的,热火朝天。可突然,几位客人等不及起身走了。问题来了:你得赶紧通知后厨“那桌的菜别做了”,但灶台忙成一锅粥,有些厨师根本不知道顾客走了,还在闷头炒一盘永远不会有人来吃的菜。灶台被占着,食材被浪费,新单子进来反而出不了餐——服务越来越慢,最后整个餐厅被拖垮。
Go 语言的 goroutine 就像后厨里的厨师,启动成本极低,go func() 一行就能让一个任务跑起来。启动容易,管住难。干活的 goroutine 一旦收不到“停手”的信号,就会一直占着内存和 CPU,像那个闷头炒菜的厨师。context 包就是 Go 官方给传菜间装的那套“大喇叭广播系统”:一个取消信号传下去,所有相关的 goroutine 收到通知,自动停手、收拾灶台、把占用的资源还回来。这套系统还内置超时机制——每道菜设定出餐时限,到点自动作废;即便有 goroutine 悄悄堆积,你也可以打开 pprof 这个“后厨监控”,逐一揪出是谁在闷头做没人要的菜。
这篇文章,我们就从这套“后厨大喇叭”讲起,彻底搞懂 Go context 的取消与超时。
掌握 context 等于掌握了 goroutine 生命周期管理的钥匙,这是初、中级 Go 开发者迈向高并发服务稳定性的分水岭技能。
可视化演示
展示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 社区的强制约定,就像每个档口的大喇叭都必须接到总线上,信号才能一路通到最角落的厨师耳边。
- Deadline(最后时限)返回该 context 的最后期限时刻;没有设置期限时 ok 为 false,goroutine 可用它判断任务还剩多少时间。
- Done(取消信号)返回一个只读通道。context 被取消时通道关闭,goroutine 通过读取它判断“该不该停手”。
- Err(取消原因)通道关闭后,用 Err 区分是被手动取消(Canceled)还是超时(DeadlineExceeded)。
- Value(随单便签)读取 context 链路上携带的键值对,像外卖单上贴的备注。
取消传播机制
现在回到那个忙碌的夜晚。领班看到客人走了,走到传菜间,按下“全店广播”按钮。接下来的事情不需要挨个通知——信号顺着线路自动传到每一个档口的喇叭,所有厨师同时听到了“停手”。context 的取消传播,就是这套“按下按钮,全员皆知”的机制。
在 Go 里,按下按钮的动作是调用 cancel 函数。用 context.WithCancel(parent) 会返回一个新 context 和它的取消函数;一旦调用 cancel(),这个 context 以及它所有的后代 context,都会收到取消信号——它们的 Done() 通道同步关闭。
注意,取消不是强杀。context 不会把正在跑的 goroutine 一枪毙掉,它只是把“停手”的信号送到。每个 goroutine 必须自己主动检查——最常见的写法是 select 同时监听业务通道和 ctx.Done(),一旦 Done() 通道关闭就从业务循环里退出来。这叫做“协作式取消”,goroutine 放下手头工作,自己走人。就像厨师听到喇叭后自己关火,而不是领班跑进来夺刀。
- cancel(p)(按下广播按钮)调用根 context 的取消函数,激活传播。
- ∀c ∈ children(p)(广播覆盖所有档口)从 p 派生的全部子 context 都会被通知,无需逐个调用。
- Done(c) 关闭(信号抵达)每个子 context 的 Done 通道关闭,等待中的 goroutine 立即苏醒并退出。
超时与期限
连锁餐厅有个规矩:每道菜必须在限定时间内上桌,超时就作废重做。后厨的大喇叭系统也内置了定时器——领班不用手动盯着表,时间一到,喇叭自动响起,“这道菜取消!”
context 提供了两种定时器:WithTimeout 设定相对时长(比如 2 秒后取消),WithDeadline 设定绝对时间点(比如 17:30:00 到点取消)。它们返回的 context 会在指定的时间点自动触发取消,等价于 Go 内部帮你调用了一次 cancel。
实际上 WithTimeout 内部实现就是调用 WithDeadline(parent, time.Now().Add(timeout))。超时的意义在于给外部依赖设定红线:比如调用下游 API 最长等 3 秒,3 秒没响应就取消这次请求。此时 ctx.Err() 返回 DeadlineExceeded,你可以据此区分“真的超时了”还是“上游主动取消”。时限到了喇叭自动响,后厨不用人盯——这就是 context 超时机制的意义。
- t_deadline(绝对期限)WithDeadline 设置的固定时刻,例如 17:30:00 整到点取消。
- t_timeout(相对超时)WithTimeout 以“当前时刻 + 时长”换算成内部的绝对时刻。
- min(先到先触发)两种期限同时存在时,谁更早触发谁生效,保证最晚不超过最早的期限。
防泄漏机制
顾客走了,厨师不知道,还在闷头炒一道永远不会被端上桌的菜。他占着灶台、耗着燃气、堆着食材。餐厅的灶台有限,炒废的菜越来越多,新客人的单反而做不出来了——这就是后厨版本的 goroutine 泄漏。
goroutine 泄漏的典型场景:一个 goroutine 阻塞在 channel 上等待消息,但发送方永远不会来了(因为请求早已结束、没人发数据),goroutine 就一直阻塞下去。泄漏的 goroutine 不会自己消失,它占着栈内存和调度器槽位,越积越多,最后服务内存暴涨、性能雪崩。
防泄漏的关键手段是 select + Done()。一个需要长时间运行的 goroutine,它的内部循环里必须监听 ctx.Done():业务通道收到数据就处理,Done 通道关闭就立刻 return。同时别忘了 defer 清理资源——比如关闭数据库连接、释放文件句柄。defer 保证即使 goroutine 中途退出,资源也会归还。每个 goroutine 都应该“耳听六路”——一边干活,一边听着大喇叭。听到“停手”就关火、收拾灶台、走人。
- N_started(启动总数)通过 go 关键字启动的 goroutine 数。启动廉价,但没人管就停不下来。
- N_finished(退出总数)函数完整返回的 goroutine 数。只有 return 才算真正结束。
- N_leak(差值)差值为正,说明存在永远卡死的 goroutine。
级联取消实战
一桌客人点了一桌菜:凉菜、热菜、汤。客人突然全走光了,领班按下广播,配菜间、炒锅、炖汤的瓦罐全部同时停手。这个取消不是孤立的——它是从“这一桌订单”这个根节点,沿着订单的每个子项传递下去,一道菜都不漏。
程序中往往不只是单个 goroutine,而是一棵由 context 构成的树:根 context 派生多个子 context,每个子 context 又派生孙 context。当根被取消,信号沿着树结构递归传导,所有层级的 goroutine 在几乎同一时刻收到通知。这就是级联取消。
级联取消还有一个重要课题:部分完成回滚。比如一个下单流程:扣库存 → 生成订单 → 调用支付。如果支付环节超时取消,前面的扣库存必须回滚。实际代码中,cancel 信号传回来时,每个环节都要用 select 监听 Done(),收到后执行自己的补偿逻辑(defer 或专门的回滚函数)。一次广播,整桌菜联动停手,还能顺手把已经做了一半的菜处理掉——这就是级联取消的威力。
- active(仍在工作的子树)记录尚未响应取消的子孙 context 集合。
- t_cancel^(c)(单点停手耗时)每个子任务从收到信号到完成清理所需的时间。
- max(最慢者决定总量)整棵树的“完全停净”时间由最慢的子任务决定。
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 链上的每个服务,共享同一根“信号线”。
- t_end - t_start(请求总耗时)服务从收到请求到处理完成的墙钟时间。
- T_ctx(超时预算)调用链上约定的取消上限。
- ≤(硬约束)请求必须在 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() 通道,就不会再一直做没人要的菜。
- Stack_i(第 i 个 goroutine 堆栈)pprof 抓取 goroutine 时记录其阻塞位置。
- [···](艾弗森括号)条件成立记 1、不成立记 0,用来做计数。
- Σ(汇总堆叠数)同一位置出现的次数越多越可疑。
最佳实践
一家运转良好的餐厅,一定有一张贴在墙上的操作守则:大喇叭保持畅通、每道菜标注时限、听到广播立即停手。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(传递)(context 进首参)调用链每一层都把 ctx 传下去,信号才能到达每一个 goroutine。
- P(cancel)(取消函数被调用)cancel 被 defer 调用,即使函数提前 return 也保证执行。
- P(清理)(资源被释放)defer 里关闭连接、通道、文件句柄。
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 演示
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("主流程结束")
child = SimContext(parent=self)每一只“子喇叭”都挂到父喇叭之下,形成 context 树。threading.Timer(seconds, child.cancel).start()模拟 WithTimeout:到点自动触发取消。if self.parent and self.parent.is_cancelled():逐级向祖先查询取消状态,模拟 Done 通道沿链传播。for child in self.children: child.cancel()cancel 函数级联通知全部子 context。if self.is_cancelled():worker 循环中的“协作式取消”判断,收到信号就收手。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() 的作用是什么?
- A.创建永不取消的根 context← 正确答案
- B.创建带超时的根节点
- C.创建可取消的 context 根
- D.创建携带数据的 context
答案:A。Background 返回一个非 nil 的空 context,没有 deadline、不会取消、没有 value,是 context 树的根。它就像大喇叭系统的总电源——常开,但本身不发声。所有可取消的 context 都要从它派生,程序的主函数、init 函数或测试入口用 Background 作为起点。
Q2. 下列哪个方法用于设置相对超时?
- A.WithTimeout 设置相对超时← 正确答案
- B.WithDeadline 设置绝对时限
- C.WithCancel 派生取消信号
- D.WithValue 携带键值数据
答案:A。WithTimeout(parent, d) 接受相对时长 d,时间到自动取消。WithDeadline 接受绝对时刻;WithCancel 只提供手动取消;WithValue 只携带键值对。超时翻译成大白话:出餐时限从按下计时器那一刻开始算。
Q3. context 的 Done() 通道何时关闭?
- A.goroutine 完成任务时自动关闭
- B.context 被 GC 回收时关闭
- C.context 被取消或超时时关闭← 正确答案
- D.传入的值被修改时关闭
答案:C。Done() 返回一个只读通道,当 context 被取消(手动 cancel)或到达 deadline/timeout 时,运行时关闭这个通道。关闭后阻塞读取立刻返回,goroutine 借它感知“该停手了”。goroutine 完成自身工作和通道关闭无关,GC 和值修改也不会影响 Done。
Q4. 以下哪项是协作式取消的正确理解?
- A.goroutine 被运行时强制终止
- B.goroutine 主动检查 Done 并退出← 正确答案
- C.context 自动回收 goroutine
- D.等待 goroutine 自然结束
答案:B。协作式取消是 goroutine 主动检查 ctx.Done(),通常写在 select 分支里:业务数据来了处理业务,Done() 关闭了立刻 return。Go 不会强杀 goroutine,所以这种主动检查是取消生效的唯一途径。大喇叭响了,厨师必须自己关火。
Q5. 调用 cancel 后,子 context 会发生什么?
- A.只有父 context 自己取消
- B.子 context 继续正常运行
- C.子 context 变成 Background
- D.所有子 context 级联取消← 正确答案
答案:D。context 的取消是树形传播的:调用 cancel,当前 context 及其所有后代 context 的 Done 通道都会关闭。子 context 不用各自调用 cancel,只要祖先链条上有人取消,它就被级联取消。这正是“一次广播,全员皆知”。
Q6. 下列哪种场景最容易造成 goroutine 泄漏?
- A.goroutine 阻塞写 channel 无接收方← 正确答案
- B.goroutine 正常执行完返回
- C.goroutine 用 select 监听 Done
- D.goroutine 用 defer 释放资源
答案:A。无缓冲 channel 的发送或接收永远找不到对方、没有超时、也没人取消,goroutine 就永久阻塞,这就是典型的泄漏。正常返回、select 监听 Done、defer 释放资源都是正确做法,不会泄漏。厨师要一直做没人取的菜,灶台才会被越占越满。
Q7. HTTP 服务端如何获取当前请求的 context?
- 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 的正确使用方式?
- A.作为函数第一个参数传递← 正确答案
- B.将 context 存结构体字段中
- C.defer 调用取消函数← 正确答案
- D.WithValue 塞大量业务数据
答案:AC。context 作为函数第一个参数是 Go 社区铁律;defer cancel() 确保取消函数被调用;存 struct 违反官方建议;WithValue 只适合少量请求级元数据,不适合塞大量业务数据。大喇叭要挂在请求上跟着走,而不是焊死在某个物件里。
Q9. 使用 pprof 排查 goroutine 堆积的正确步骤包括哪些?
- A.引入 net/http/pprof 包← 正确答案
- B.访问 goroutine 采样端点← 正确答案
- C.分析堆栈定位阻塞点← 正确答案
- D.直接重启服务器解决
答案:ABC。导入 net/http/pprof 并启动 HTTP 服务后,访问 /debug/pprof/goroutine 抓取所有 goroutine 堆栈,通过 top/list 分析哪些位置堆积最多,修复后重新采样验证。重启服务器只是治标,不能定位根因,就像把监控录像删了而不是去找那位闷头炒菜的厨师。
Q10. 关于 context 超时与取消,以下说法正确的有哪些?
- A.WithDeadline 设置绝对时间点← 正确答案
- B.父取消会级联取消子节点← 正确答案
- C.context 能强制终止 goroutine
- D.超时时间越大任务越安全
答案:AB。WithDeadline 设置绝对时间点(如 17:30:00),父 context 取消时子 context 级联取消是内置语义。context 没有强杀 goroutine 的能力,只能发信号;超时设得很大反而增加资源占用和拖慢故障响应,不是“越大越安全”。