context.Cause 不存在于标准库,ctx.Err()仅返回预定义错误;需自定义结构体包装Context并重写Err()和添加Cause()方法,或改用结构化日志记录取消原因。

Context取消后,context.Cause 并不存在
Go 标准库的 context.Context 接口本身**不提供获取取消原因的方法**。你调用 ctx.Err() 只能得到 context.Canceled 或 context.DeadlineExceeded 这两个预定义错误,无法区分“为什么被取消”——比如是用户主动调用 cancel()、超时触发、还是某个中间件因错误提前终止。
想传取消原因,得自己封装 context.Context
标准 context.WithCancel 和 context.WithTimeout 不支持携带额外信息。可行做法是:用一个自定义结构体包装原始 context.Context,并在取消时把原因存入字段,再通过自定义方法暴露出来。
常见实操建议:
- 定义一个带
cause error字段的结构体,实现context.Context接口(只需代理Deadline、Done、Err、Value) - 重写
Err()方法:如果已取消,优先返回你存的cause;否则 fallback 到底层ctx.Err() - 提供一个显式的
Cause()方法(返回error),供调用方安全读取 - 取消函数要同时触发原
cancel()和设置cause字段(注意并发安全,建议用sync.Once或原子写)
示例关键片段:
type causeCtx struct {
context.Context
cause error
once sync.Once
}
func (c *causeCtx) Err() error {
if err := c.Context.Err(); err != nil {
return c.cause
}
return err
}
func (c *causeCtx) Cause() error {
return c.cause
}
func WithCause(parent context.Context, cause error) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(parent)
c := &causeCtx{Context: ctx, cause: cause}
return c, func() {
c.once.Do(func() { cancel() })
}
}
第三方库如 golang.org/x/net/context 也不行,别白费力气
golang.org/x/net/context 是旧包,早已被标准库 context 取代,且它同样没有 Cause 支持。目前社区较成熟的方案只有:go.uber.org/zap 的 zap.Error 日志上下文、或 github.com/cockroachdb/errors 这类错误包装库配合手动传递——但它们都不修改 context.Context 本身。
所以如果你看到有人用 ctx.Value("cancel_reason") 尝试传递,要注意:这不能覆盖 Err() 行为,且容易被中间件覆盖或遗漏,可靠性远低于自定义 Context 实现。
真正需要取消原因时,通常意味着你已在处理失败路径
多数 HTTP handler 或 RPC server 场景中,你真正关心的不是“谁取消了”,而是“这次请求失败是否可重试”“要不要记录告警”。这时候更务实的做法是:在触发 cancel() 前,把原因作为结构化日志打出去(比如用 zap.String("cancel_cause", "user_logout")),而不是塞进 Context 等下游去查。
自定义 Context 虽然可行,但会增加调用链理解成本;若团队没统一约定,下游很可能忽略 Cause() 方法,继续只看 Err()。这点在跨服务或中间件较多的系统里尤其明显。

















