Go 1.21 context.WithoutCancel 实战:父 context 取消了,收尾任务还要继续跑

Go 1.21 context.WithoutCancel 实战:父 context 取消了,收尾任务还要继续跑

HTTP 请求处理到一半,客户端断开连接,r.Context() 被取消------这是好事,能及时停掉没用的工作。但有个尴尬场景:你在这个请求里还想异步记一条审计日志、或者把结果写回缓存,这些收尾工作不该 跟着请求一起被取消。Go 1.21 新加的 context.WithoutCancel 就是为这个来的。这篇讲清楚它解决什么、怎么用,以及顺带认识同批加入的 WithDeadlineCauseCause

先看问题:取消会顺着 context 树往下传

Go 的 context 取消是级联的:父 context 一取消,所有从它派生的子 context 全部取消。平时这正是我们想要的,但看这段:

go 复制代码
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    result := doWork(ctx)

    // 想异步记审计日志,但用了同一个 ctx
    go func() {
        // 如果客户端此刻断开,ctx 被取消,这条日志就写不进去了
        writeAuditLog(ctx, result)
    }()

    w.Write(result)
}

问题:writeAuditLog 用的是请求的 ctx。一旦客户端断连或请求结束,ctx.Done() 触发,数据库驱动看到 context 取消会直接放弃写入,审计日志丢了。而审计日志恰恰是"请求都结束了也得落库"的东西。

朴素的错误修法:直接用 context.Background()

很多人会这样绕过:

go 复制代码
go func() {
    writeAuditLog(context.Background(), result)   // 换成全新的 ctx
}()

日志确实能写了,但你把父 context 里携带的所有值 都丢了:trace ID、租户信息、请求级的 logger......全没了。因为 context.Background() 是一张白纸,它不知道原来那条链上挂了什么。审计日志里少了 trace ID,出了问题根本关联不起来。

正确写法:context.WithoutCancel

WithoutCancel 返回一个新的 context:它保留父 context 的所有 Value,但切断取消信号。父被取消,它不受影响;父携带的值,它照样能读。

go 复制代码
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    result := doWork(ctx)

    // 派生一个"不会被取消"的 ctx,但 trace ID 等值都还在
    detached := context.WithoutCancel(ctx)

    go func() {
        // 请求结束/客户端断连都不影响它,审计日志照样落库
        writeAuditLog(detached, result)
    }()

    w.Write(result)
}

验证一下它的行为:

go 复制代码
func main() {
    parent, cancel := context.WithCancel(context.Background())
    parent = context.WithValue(parent, "traceID", "abc-123")

    detached := context.WithoutCancel(parent)

    cancel() // 取消父 context

    fmt.Println("parent Err:  ", parent.Err())              // context canceled
    fmt.Println("detached Err:", detached.Err())            // <nil> ------ 没被取消
    fmt.Println("traceID:     ", detached.Value("traceID")) // abc-123 ------ 值还在
}

detached.Err() 是 nil、detached.Done() 永远不会关闭,但 traceID 依然读得到。这就是它和 context.Background() 的本质区别:一个继承了值,一个是白纸。

别忘了给脱离的任务单独设超时

WithoutCancel 切断的是"来自父的取消",不代表这个任务可以永远跑下去。收尾任务同样可能卡住,所以通常再给它套一个自己的超时:

go 复制代码
detached := context.WithoutCancel(ctx)
// 给收尾任务一个独立的 10 秒预算,防止它自己卡死
bgCtx, bgCancel := context.WithTimeout(detached, 10*time.Second)
go func() {
    defer bgCancel()
    writeAuditLog(bgCtx, result)
}()

这样:父取消 → 不影响;但收尾任务超过 10 秒 → 自己被取消。两条超时线互相独立,互不干扰。

顺带:同批加入的 WithDeadlineCause 和 Cause

Go 1.21 还补齐了"取消原因"这条链。以前你只知道 context 被取消了(context.Canceled),但不知道为什么。现在可以带上原因:

go 复制代码
ctx, cancel := context.WithTimeoutCause(
    context.Background(), 100*time.Millisecond,
    errors.New("下游数据库连接超时"), // 自定义原因
)
defer cancel()

<-ctx.Done()
fmt.Println(ctx.Err())            // context deadline exceeded(标准错误)
fmt.Println(context.Cause(ctx))  // 下游数据库连接超时(你的真实原因)

ctx.Err() 返回的还是标准的 DeadlineExceeded,但 context.Cause(ctx) 能拿到你埋进去的具体原因。排查线上超时时,这一句话能省掉一堆猜测。手动取消也一样:cancel(errors.New("用户主动关闭")) 配合 context.WithCancelCause 使用。

什么时候用、什么时候别用

  • 该用 WithoutCancel:审计日志、指标上报、缓存回写、清理临时资源------这些"请求结束也要做完"、且需要保留 trace 上下文的收尾工作。
  • 不该用:主链路上的正常调用。如果一个操作本就该随请求取消而停止(比如查询数据库返回给用户),用它反而会让无用工作继续跑,浪费资源。
  • 记得配超时 :脱离取消 ≠ 可以无限跑,几乎总要再叠一层 WithTimeout

小结

  • context.WithoutCancel(Go 1.21)派生一个保留父 Value 但不继承取消信号的 context,专治"请求结束了收尾工作还得跑完"。
  • 它和 context.Background() 的关键区别:后者丢掉所有值(trace ID 等),前者原样继承。
  • 脱离取消的任务通常要再套 WithTimeout,给它一条独立的超时线,别让它卡死。
  • 同批的 WithTimeoutCause / WithCancelCause + context.Cause 能带上并取回具体取消原因,排查超时不再靠猜。
  • 一句话记忆:WithoutCancel = 只断取消、不丢上下文;用它做收尾,别忘了给收尾也设个闹钟。
相关推荐
源代码•宸1 小时前
前置准备:定时微服务有什么价值
经验分享·后端·微服务·云原生·架构
wdfk_prog1 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
catino1 小时前
高并发详解
后端
启观川1 小时前
Python基础-第 16 章 综合案例:客户信息管理系统
开发语言·笔记·python·正则表达式
掘金者阿豪1 小时前
GPT-6 Astra 来了,GPT-5.6 Sol 还值得用吗?聊聊 Coding、百万上下文、价格和 Plus/Pro
前端·后端
无小道1 小时前
C/C++——可变参数
c语言·开发语言·c++·可变参数
BioRunYiXue2 小时前
科研干货 | IC50全面解读:概念解析、实验设计与数据分析要点
java·开发语言·javascript·人工智能·算法·数据挖掘·数据分析
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day34|AOP 进阶、权限思维导图复盘
java·开发语言·spring boot·学习
SamDeepThinking2 小时前
HashMap 分组操作的演进:从三次查找到一次调用
java·后端·程序员