必须用 ctx.Err() == context.Canceled 或 == context.DeadlineExceeded 判断,因二者是导出变量,需精确匹配;仅 err != nil 无法区分取消与超时,影响重试等业务决策;字符串比较易受本地化和版本变更影响。

直接用 ctx.Err() == context.Canceled 或 ctx.Err() == context.DeadlineExceeded 判断,不能只写 err != nil,更不能比对错误字符串。
为什么不能只写 err != nil
因为 ctx.Err() 返回非 nil 仅表示“已取消或超时”,但你通常需要区分是主动取消还是超时——比如重试逻辑中,context.Canceled 往往不该重试,而 context.DeadlineExceeded 可能要退避重试。只判 != nil 会把两者混为一谈。
-
ctx.Err()在 Context 尚未取消、也无 deadline 时返回nil - 一旦
ctx.Done()关闭,ctx.Err()才会返回具体错误值 -
context.Canceled和context.DeadlineExceeded是包级导出变量,不是临时构造的错误,必须用==比较
为什么不能用 err.Error() == "context canceled"
Go 1.22+ 起错误消息可能本地化(比如中文环境返回 "上下文已被取消"),字符串匹配直接失效;而且标准库不承诺错误文本稳定,未来版本可能微调措辞。
- 错误消息是面向开发者调试用的,不是 API 合约
-
errors.Is(err, context.Canceled)虽然也能用,但绕路:既然有精确的导出变量,直接==更轻、更直观、无额外开销 - 别在日志里拼接
err.Error()做条件分支,这是常见翻车点
什么时候该检查 ctx.Err()?
它适合做“快照式”判断,比如函数入口校验、循环体内的定期轮询;但不能替代监听 ctx.Done()。
- 耗时操作(如
time.Sleep、http.Do、数据库查询)前/中/后都应检查,避免卡死 - 若操作本身不支持传入
ctx(如os.ReadFile),必须在循环中显式调用ctx.Err(),否则无法响应中途取消 - 注意竞态:先查
ctx.Err()再进select监听ctx.Done(),中间存在窗口期,可能错过取消信号
子 goroutine 里怎么安全判断父 Context 是否取消
别直接用 r.Context()(比如 HTTP handler 中拿到的),尤其当你启动子 goroutine 做异步任务时。
- 必须基于父
ctx派生新ctx,例如ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - 子 goroutine 结束后记得调
cancel()(常配合defer),防止计时器泄漏 - 如果子任务需独立生命周期,用
context.WithCancel(r.Context())并由父 goroutine 控制何时调cancel() - 绝对不要在子 goroutine 里写
ctx = context.Background()—— 这等于主动切断取消链
最易被忽略的一点:判断只是起点,真正关键的是后续动作是否与错误类型匹配。比如 context.DeadlineExceeded 后要不要重试、退避、降级,和 context.Canceled 的处理路径完全不同;而很多 bug 就出在写了判断,却没对应执行清理或分支逻辑。

















