Go context 怎样为取消原因建立可区分的错误类型
来源:17golang原创
时间:2026-10-18 05:38:03 449浏览 收藏
我在把一条请求链拆成多个 goroutine 时,最容易丢掉的不是取消信号,而是“为什么取消”。只看 ctx.Err(),主动取消和业务失败都可能只得到 context canceled。更合适的做法是用 context.WithCancelCause 创建子 Context:ctx.Err() 保留通用取消类别,context.Cause(ctx) 保留可区分的错误对象。
官方文档地址:https://pkg.go.dev/context。这组 API 从 Go 1.20 开始可用;本文只讨论标准库 context 的取消原因,不引入第三方替代方案。
需要让下游识别业务原因时,调用WithCancelCause,取消时传入哨兵错误或自定义错误;下游用errors.Is(context.Cause(ctx), ErrXxx)判断原因,同时仍用ctx.Err()判断 Context 是否已取消。
先把 Cause 和 Err 分成两条信息
package main
import (
"context"
"errors"
"fmt"
)
var ErrQuotaReached = errors.New("quota reached")
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil) // 兜底释放资源;已经取消时不会覆盖首个原因。
cancel(ErrQuotaReached) // 把业务原因写入这个 Context。
fmt.Println(ctx.Err() == context.Canceled)
fmt.Println(errors.Is(context.Cause(ctx), ErrQuotaReached))
}
这段代码表达两层语义:ctx.Err() 返回 context.Canceled,适合让统一的取消分支停止工作;context.Cause(ctx) 返回 ErrQuotaReached,适合记录指标、选择重试策略或向调用方映射业务结果。两者不是互相替代的字段。

错误类型要稳定,调用方才不用比较字符串
如果原因只有几个固定类别,用包级哨兵错误就足够;如果还要携带任务编号等信息,可以定义自己的错误类型,并实现 Unwrap 保留分类关系。关键是把“类别”放在错误类型或包装链里,不要让下游依赖 err.Error() 的文字。
package task
import (
"context"
"errors"
"fmt"
)
var ErrUserStopped = errors.New("user stopped")
type Failed struct {
JobID string
Err error
}
func (e *Failed) Error() string { return fmt.Sprintf("job %s failed: %v", e.JobID, e.Err) }
func (e *Failed) Unwrap() error { return e.Err } // 让 errors.Is 继续识别底层类别。
func classify(err error) string {
switch {
case errors.Is(err, ErrUserStopped):
return "主动停止"
case errors.Is(err, context.DeadlineExceeded):
return "超时"
default:
return "其它失败"
}
}
上面的示例中,Failed 可以携带业务上下文,Unwrap 让 errors.Is 仍能匹配 ErrUserStopped 等稳定类别。实际文件还需要导入 context;这里把类型设计和分类函数放在一起,是为了突出错误契约,避免把分类写成字符串比较。
下游读取时只认首个取消原因
CancelCauseFunc 的原因由第一次取消确定。父 Context 先取消,子 Context 会继承父原因;子 Context 如果先取消,则可以保留自己的原因,之后父 Context 再取消也不会改写它。这个规则适合把更靠近故障现场的原因传给下游,但也意味着取消顺序是接口契约的一部分。
func reason(ctx context.Context) error {
if err := ctx.Err(); err != nil {
cause := context.Cause(ctx)
if cause == nil { // 尚未取消时不会走到这里,保留防御性判断。
return err
}
return cause
}
return nil
}
// 父 Context 取消后,派生的 child 会看到同一个取消原因。
func childWork(ctx context.Context) error {
select {
case

如果只是调用 cancel(nil),原因会落为 context.Canceled;如果是超时构造器自然到期,则应按对应的超时语义处理。无论哪种情况,都要在创建带取消或超时的 Context 后及时调用返回的取消函数,避免把计时器和父子引用留到更晚。
迁移时记住这五个边界
WithCancelCause与Cause从 Go 1.20 提供;WithTimeoutCause和WithDeadlineCause从 Go 1.21 提供。- 调用
cancel只负责发出取消信号,不等待业务 goroutine 自己退出。 - 错误类别使用哨兵错误或类型,再用
errors.Is/errors.As判断,不比较展示字符串。 - 下游既需要停止工作,又需要区分原因时,同时保留
ctx.Err()和context.Cause(ctx)两条判断。 - 测试或日志可以覆盖“父先取消、子先取消、nil 原因”三种边界,但不要把示例代码的输出写成本文已经执行的证据。
两个容易混淆的问题
为什么 Cause 不是直接替代 Err? 因为 Err 是 Context 接口约定的通用状态,便于所有下游统一处理;Cause 才是可选的业务解释。只记录 Cause 会让通用取消分支失去稳定判断。
子 Context 能覆盖父 Context 的原因吗? 只有子 Context 在父 Context 取消前先被取消,才会保留自己的原因;父先取消后,子会继承已经确定的父原因。
这套写法的价值不在于把错误描述得更长,而在于把“取消状态”和“取消原因”拆成稳定的两个契约:上层负责决定原因,下游负责用类型判断并及时退出。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
437 收藏
-
478 收藏
-
367 收藏
-
230 收藏
-
499 收藏
-
330 收藏
-
385 收藏
-
318 收藏
-
163 收藏
-
244 收藏
-
118 收藏
-
433 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习