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

context.Cause 区分主动取消与超时取消

来源:17golang原创

时间:2026-10-11 00:37:17 485浏览 收藏

Go 里遇到请求被取消,先看 ctx.Err() 只能得到两类稳定结果:context.Canceled 或 context.DeadlineExceeded。如果还想知道“是谁因为什么原因主动结束了工作”,应该读取 context.Cause(ctx)。

官方资料:https://pkg.go.dev/context/

从 Go 1.20 开始,context.WithCancelCause 可以记录一个错误原因,之后用 context.Cause 读取。超时场景仍然可以用 WithTimeout;如果业务需要给超时附加自己的原因,则使用 Go 1.21 引入的 WithTimeoutCause。实战中可以把 Err 用作流程分支,把 Cause 用作日志和排障信息。

先分清 Err 和 Cause

Err 回答的是“上下文属于哪一种取消状态”,Cause 回答的是“这次取消记录的具体错误是什么”。两者并不互相替代:自定义原因不会改变 ctx.Err() 返回的兼容类别。

读取方式用途典型结果
ctx.Err()流程分支、兼容旧接口context.Canceled / context.DeadlineExceeded
context.Cause(ctx)日志、指标、故障定位自定义错误或与 Err 相同的错误
主动取消与超时取消分别通过 Err 和 Cause 读取的结构说明图
图1:主动取消与超时取消的原因读取说明图;这是静态说明图,不是运行截图。

用 WithCancelCause 记录主动取消原因

主动取消常见于上游请求结束、用户停止任务或服务准备关闭。用普通的 WithCancel 时,调用方只能看到 context.Canceled;改用 WithCancelCause 后,仍可用 errors.Is 判断取消类别,同时可以保留业务错误。

package main

import (
	"context"
	"errors"
	"fmt"
)

var errUserStopped = errors.New("user stopped export")

func main() {
	// CancelCauseFunc 同时结束上下文,并记录主动取消的具体原因。
	ctx, cancel := context.WithCancelCause(context.Background())
	cancel(errUserStopped)

	// Err 维持稳定的取消类别,适合让上层决定是否重试或退出。
	fmt.Println(errors.Is(ctx.Err(), context.Canceled))
	// Cause 保留业务错误,适合写入日志或诊断字段。
	fmt.Println(context.Cause(ctx))
}

这段代码的两个输出分别表达“确实是取消”和“取消原因是用户停止导出”。不要把 Cause 的字符串直接拿来替代 errors.Is;上层分支应保留错误链判断,日志才补充具体原因。

用超时上下文识别 DeadlineExceeded

超时由上下文的截止时间触发,最常用的写法是 WithTimeout。调用返回后应先判断 ctx.Err() 是否为 context.DeadlineExceeded,再读取 Cause。如果没有额外指定原因,二者会得到同一个超时错误。

package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

func main() {
	// 100 毫秒后自动取消,defer 负责提前完成时释放计时器资源。
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	// 模拟一个没有及时完成的工作,让超时分支收到取消信号。
	

如果想让超时带上更明确的业务原因,可以使用 context.WithTimeoutCause。它的取消函数只负责提前结束上下文,不会覆盖已经设定的超时原因,所以不要把普通 CancelFunc 当成设置原因的入口。

// WithTimeoutCause 让超时原因带上调用方可识别的业务语义。
ctx, cancel := context.WithTimeoutCause(parent, 100*time.Millisecond, errBackendSlow)
defer cancel() // 即使提前返回也要释放定时器资源

父子上下文为什么不能只看谁最后取消

取消原因由“第一次取消”决定。父上下文先取消时,子上下文会继承父的原因;子上下文先取消时,子保留自己的原因,父上下文之后再取消也不会改写已经结束的子上下文。这个规则保证了原因不会在并发传播过程中被后来事件覆盖。

package main

import (
	"context"
	"errors"
	"fmt"
)

func main() {
	parent, cancelParent := context.WithCancelCause(context.Background())
	defer cancelParent(nil)
	child, cancelChild := context.WithCancelCause(parent)
	defer cancelChild(nil)

	parentCause := errors.New("request closed")
	childCause := errors.New("worker stopped")

	// 让子上下文先结束,子会保留自己的原因。
	cancelChild(childCause)
	// 父上下文随后结束,只影响父及尚未独立结束的后代。
	cancelParent(parentCause)

	fmt.Println(context.Cause(parent))
	fmt.Println(context.Cause(child))
}

这个例子输出父的 request closed 和子的 worker stopped。如果把两次取消调用顺序交换,子就会继承父的原因。也就是说,排障时要把上下文树和取消时序一起记录。

父先取消与子先取消时 context.Cause 第一次取消规则的时间线说明图
图2:父子上下文取消原因优先级说明图;这是静态说明图,不是运行截图。

把取消原因接到日志与返回错误

建议把取消处理拆成两层:第一层用 errors.Is 判断是否主动取消或超时,第二层用 context.Cause 生成诊断字段。这样既不会破坏调用方已有的错误分支,又能在日志里区分“用户停止”“上游请求结束”和“后端响应过慢”。

func classify(ctx context.Context) string {
	// 未取消时不生成误导性的原因标签。
	if ctx.Err() == nil {
		return "running"
	}

	// 先按稳定类别分支,避免依赖自定义错误的字符串。
	switch {
	case errors.Is(ctx.Err(), context.DeadlineExceeded):
		return fmt.Sprintf("timeout: %v", context.Cause(ctx))
	case errors.Is(ctx.Err(), context.Canceled):
		return fmt.Sprintf("canceled: %v", context.Cause(ctx))
	default:
		// 保留未知错误,便于发现不符合预期的上下文实现。
		return fmt.Sprintf("unknown: %v", ctx.Err())
	}
}

这里的 context.Cause(ctx) 只在上下文已取消后读取。如果上下文尚未取消,它会返回 nil;因此不要在任务开始时把它当成必有值的错误。

常见问题

WithCancelCause 会让 ctx.Err() 返回自定义错误吗?

不会。ctx.Err() 仍返回 context.Canceled,自定义错误通过 context.Cause(ctx) 读取。

普通 WithTimeout 能区分主动取消和超时取消吗?

能通过 errors.Is(ctx.Err(), context.DeadlineExceeded) 判断超时;若还要附加业务原因,可以改用 WithTimeoutCause。

为什么子上下文的 Cause 和父上下文不同?

子上下文可能在父取消前已经被独立取消。每个上下文都由自己的第一次取消确定原因,父子关系只在取消传播发生时生效。

应该把 Cause 写进接口返回给客户端吗?

不建议直接暴露内部错误文本。可以用 Err 映射稳定的公开状态,再把 Cause 留在日志、指标或内部追踪字段中。

总结一下:ctx.Err() 负责稳定分类,context.Cause(ctx) 负责解释原因;主动取消使用 WithCancelCause,带业务语义的超时使用 WithTimeoutCause,父子上下文则要按第一次取消的时序理解原因来源。

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