context.withcancel 是跨协程取消的起点,通过返回的 cancel() 函数关闭 done() 通道实现自动通知;需在父 goroutine 显式调用且避免重复调用,取消原因需额外传递。

context.WithCancel 是跨协程取消的起点
跨协程取消不是靠手动 channel 通知,而是靠 context.Context 的 Done() 通道自动关闭来触发。核心是用 context.WithCancel 创建可取消的上下文,它返回一个 context.Context 和一个 cancel 函数;调用 cancel() 后,所有监听该 context 的 goroutine 都能立刻感知。
常见错误是只传 context 却忘了在适当位置调用 cancel(),或者在多个 goroutine 中重复调用 cancel()(虽不 panic,但可能干扰逻辑)。
- 必须在父 goroutine 显式调用
cancel(),子 goroutine 不负责触发取消 -
cancel()可安全多次调用,后续调用无效果,但不应依赖此行为 - 若需传递取消原因,应额外用 error channel 或字段配合,
context本身不携带取消原因
监听 Done() 时别漏掉
ctx.Done() 返回一个只读 channel,一旦 context 被取消,该 channel 就会关闭, 立即返回(零值)。但很多人误以为要“主动检查”,结果写成轮询或忘记 select 配合 default,导致 goroutine 无法及时退出。
典型错误写法:if ctx.Err() != nil { return } —— 这只能捕获已发生的错误,无法响应未来取消;正确做法是用 select 等待 ctx.Done() 关闭。
- 必须用
select { case 模式监听,不能只查 <code>ctx.Err() - 如果 goroutine 还在做耗时操作(如网络请求、文件读写),需确保该操作本身支持 context(如
http.NewRequestWithContext、os.OpenFile不支持,但os.Open后的读写需自行检查ctx.Err()) - 避免在
select中遗漏default导致阻塞,除非你明确希望等待取消
嵌套 context 时注意 WithCancel / WithTimeout / WithDeadline 的传播规则
子 context 的取消完全依赖父 context:父被取消,所有子自动取消;但子调用 cancel() 不会影响父或其他兄弟 context。这意味着你可以安全地为每个 goroutine 分配独立子 context,而不会互相污染。
容易混淆的是 context.WithTimeout 和 context.WithDeadline:它们底层也调用 WithCancel,并启动一个 timer,时间到自动调用 cancel;但如果你提前手动调用返回的 cancel(),timer 会被停掉——这是设计使然,不是 bug。
-
context.WithValue不影响取消链,仅用于传值;取消能力只来自WithCancel及其衍生函数 - 不要把不同来源的 context 混着传(比如一个来自 HTTP 请求,一个来自定时器),应统一用
context.Background()或context.TODO()作为根,再派生 - HTTP handler 中收到的
r.Context()已绑定请求生命周期,直接使用即可,无需再WithCancel包一层,除非你要加额外超时
取消后 ctx.Err() 返回值有且仅有两个有效状态
ctx.Err() 在 context 未取消时返回 nil;取消后只可能返回 context.Canceled 或 context.DeadlineExceeded。没有其他合法值,也不应自定义 error 替换它。
有人试图用 context.WithValue(ctx, key, err) 传递取消原因,这违反 context 设计原则:value 是只读元数据,不是控制流信号。真正需要区分取消类型时,应检查具体错误值,而不是覆盖它。
- 判断是否因超时取消:
errors.Is(ctx.Err(), context.DeadlineExceeded) - 判断是否被主动取消:
errors.Is(ctx.Err(), context.Canceled) - 永远不要用
==直接比较ctx.Err()和context.Canceled,要用errors.Is
最常被忽略的一点:context 取消是“单向广播”,不可恢复。一旦 cancel() 被调用,Done channel 关闭,Err() 返回非 nil,此后任何对该 context 的操作都不会逆转这个状态。所以别在取消后还把它传给新启动的 goroutine——它已经失效了。











