登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

上下文错误

来源:17golang原创

时间:2026-11-11 16:23:46 222浏览 收藏

Go 里遇到“上下文错误”,先不要看 err.Error() 的文字。最稳定的分类方式是使用 errors.Is(err, context.Canceled) 判断调用者或父上下文主动取消,使用 errors.Is(err, context.DeadlineExceeded) 判断截止时间已经耗尽;如果还需要知道取消的业务原因,再读取 context.Cause(ctx)。这三个信息层次不同,不能互相替代。

要点速览
  • context.Canceled 表示非 deadline 原因的取消,context.DeadlineExceeded 表示 deadline 到期。
  • 错误可能被数据库、HTTP 或业务层包装,所以优先用 errors.Is,不要比较字符串。
  • ctx.Err() 适合判断当前上下文状态,context.Cause(ctx) 适合追踪更具体的取消原因。

先把两个标准错误对应到触发条件

Context 的取消信号只有在 Done 关闭后才有错误结果。标准库把“截止时间经过”与“其它原因取消”分成两个哨兵错误:

错误代表的边界常见触发点
context.Canceled非 deadline 的取消调用 cancel()、父上下文被取消、调用者不再需要结果
context.DeadlineExceeded截止时间已经经过WithTimeout 或 WithDeadline 到期

子上下文的结果取最先发生的取消事件。如果父上下文先到期,子上下文即使自己还配置着更晚的时间,也应按 DeadlineExceeded 处理;如果代码先调用了返回的取消函数,则是 Canceled。因此不要只根据“这是一个 timeout 上下文”来猜错误类别。

Go Context、WithCancel、WithTimeout、Done 与 Err 的静态关系说明图
图1:上下文边界结构说明图,查看父 Context、派生取消函数、deadline 和 Err 之间的静态关系;这是说明图,不是运行截图。

用 errors.Is 判断被包装的上下文错误

下游函数经常会给错误增加调用位置或资源信息,例如 fmt.Errorf("查询用户: %w", err)。这时错误的展示文本已经变化,但底层仍然可能是 context.Canceled 或 context.DeadlineExceeded。用 errors.Is 沿着包装链判断,才能在日志可读性和机器分流之间同时保留信息。

func classify(err error) string {
    // 先识别上下文的两个标准分类,兼容下游用 %w 包装后的错误。
    switch {
    case errors.Is(err, context.Canceled):
        return "caller-canceled"
    case errors.Is(err, context.DeadlineExceeded):
        return "deadline-exceeded"
    case err == nil:
        return "ok"
    default:
        // 非上下文错误继续交给上层按业务类型处理。
        return "other"
    }
}

判断顺序不应写成字符串比较,也不建议把所有超时都转换成同一个自定义错误。调用方可能需要对主动取消停止重试,对 deadline 超时做有限重试,而对连接失败或参数错误采取完全不同的动作。

结合 ctx.Err 处理请求边界

在循环或 select 中,ctx.Err() 是读取当前上下文最终分类的直接入口:上下文仍然有效时返回 nil,结束后返回两个标准错误之一。它适合放在“停止继续工作”的返回路径;真正从下游拿到的 err 则仍应使用 errors.Is,因为中间层可能包装或替换错误。

func waitForWork(ctx context.Context, jobs 

如果一个函数接收了父上下文,就应继续把它传给数据库、HTTP 客户端或其它下游调用,而不是重新创建 context.Background()。否则上游取消和 deadline 无法到达真正执行工作的地方,最后的错误分类也会失真。

Go ctx.Err、errors.Is、context.Canceled 与 context.DeadlineExceeded 的错误分类结构图
图2:错误分类结构说明图,查看 ctx.Err 的标准分类、errors.Is 的包装判断和业务原因的分层关系;这是静态结构图,不是运行结果。

需要业务原因时读取 context.Cause

从 Go 1.20 起,context.WithCancelCause 可以在取消时记录一个更具体的原因,之后通过 context.Cause(ctx) 读取。它不会把 ctx.Err() 的标准分类改成业务错误:主动取消仍然是 context.Canceled,业务错误放在 Cause 中。deadline 场景也可以使用带 cause 的构造函数来补充说明。

func runWithCause(parent context.Context) error {
    // 标准 Err 用来做稳定分流,Cause 用来保留更具体的失败原因。
    ctx, cancel := context.WithCancelCause(parent)
    defer cancel(nil) // 正常收尾时释放关联资源,不额外覆盖已有原因

    if err := prepare(ctx); err != nil {
        cancel(fmt.Errorf("准备阶段失败: %w", err))
        return err
    }
    if err := ctx.Err(); err != nil {
        // 取消分类与业务原因分开读取,避免把 Cause 当作 Err 比较。
        return fmt.Errorf("上下文结束: %w; 原因: %v", err, context.Cause(ctx))
    }
    return nil
}

如果只是为了判断“该不该继续”,读取 ctx.Err() 就够了;如果日志、指标或上层提示还需要知道是谁触发取消,再读取 context.Cause。不要把 Cause 的具体文字当作稳定枚举,业务代码仍应围绕自己的 sentinel error 或类型做 errors.Is、errors.As 判断。

把判断逻辑收拢到错误处理函数

在服务边界统一分流,可以避免每个调用点都重复一套字符串判断。主动取消通常不适合记成服务故障,也不应因为客户端已经断开就继续重试;deadline 超时则需要结合幂等性、剩余预算和下游状态决定是否重试。

func actionFor(err error) string {
    // 取消和超时都属于上下文结束,但运营含义并不相同。
    switch {
    case errors.Is(err, context.Canceled):
        return "stop-without-retry"
    case errors.Is(err, context.DeadlineExceeded):
        return "retry-only-if-idempotent"
    default:
        return "handle-by-domain-error"
    }
}

可以把下面这条速查规则放进项目的错误处理约定:先用 errors.Is 判断标准上下文错误,再根据请求是否幂等决定重试;需要解释取消来源时读取 context.Cause;所有派生上下文都在生命周期结束时调用 cancel。

相关问题

能不能直接比较 err.Error()?

不建议。错误可能被包装,文本也可能来自不同下游实现;使用 errors.Is 才能保留底层分类。

调用 cancel() 后一定是 Canceled 吗?

对当前派生上下文主动调用其取消函数时通常是 context.Canceled;但如果父上下文更早因 deadline 结束,子上下文会继承先发生的 DeadlineExceeded。

为什么既看 err 又看 ctx.Err?

err 描述下游调用返回的错误,ctx.Err() 描述当前上下文状态。前者可能被包装,后者更适合确认请求边界是否已经结束。

Cause 会替代 DeadlineExceeded 吗?

不会。Cause 用于补充取消原因,标准分类仍由 Err 返回;两者应分别用于分流与解释。

参考文档:https://pkg.go.dev/context;https://go.dev/src/context/context.go

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>