context.Cause 区分主动取消与超时取消
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 相同的错误 |

用 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。如果把两次取消调用顺序交换,子就会继承父的原因。也就是说,排障时要把上下文树和取消时序一起记录。

把取消原因接到日志与返回错误
建议把取消处理拆成两层:第一层用 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,父子上下文则要按第一次取消的时序理解原因来源。
VS Code Tasks 组合前后端命令的依赖关系
- 上一篇
- VS Code Tasks 组合前后端命令的依赖关系
- 下一篇
- llama.cpp 量化格式选择与上下文长度配置
-
- Golang · Go问答 | 13分钟前 | 并发 · go · Go sync.Mutex 指针接收者 copylocks 并发排障
- sync.Mutex 复制后出现异常解锁的结构体设计
- 448浏览 收藏
-
- Golang · Go问答 | 22分钟前 | 并发 · go · Go wait add sync.WaitGroup Done 并发收尾 WaitGroup.Go
- WaitGroup Go 方法调用顺序的并发收尾
- 455浏览 收藏
-
- Golang · Go问答 | 53分钟前 | 并发 · go · Context · Go context Context.Value WithValue
- context.WithValue 键类型冲突导致字段覆盖的规避
- 277浏览 收藏
-
- Golang · Go问答 | 1小时前 | go ·
- context.AfterFunc 回调未执行时的取消时序
- 342浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- HTTP Trailer 读取为空时的响应头声明顺序
- 448浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- HTTP 服务器读取请求体超时的连接处理
- 290浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- net/http 客户端关闭连接后请求体重用的限制
- 497浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- unsafe.Slice 长度计算错误导致越界的定位
- 298浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 408次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 487次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 494次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 443次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 271次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览
-
- Go strings.ToValidUTF8 清洗日志内容的边界
- 2026-10-03 501浏览
-
- Go tls.GetCertificate 为什么收不到空 ServerName 请求
- 2026-09-27 501浏览

