Golang中 context 包核心 API 详解

我们看得见城市的繁华,却再也摸不着从前的回忆

Posted by yishuifengxiao on 2026-12-08

context 包用于在 goroutine 之间传递取消信号、超时信号以及请求范围内的数据。

Context 接口回顾

所有 context 都实现同一个接口:

type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回截止时间,没有则 ok=false
Done() <-chan struct{} // 取消或超时时被 close,可用来 select 监听
Err() error // Done 未关闭返回 nil;否则返回 Canceled 或 DeadlineExceeded
Value(key any) any // 读取值,链式向上查找
}

要点:

  • Done() 返回的 channel 只会被 close,不会发送值。
  • Err() 只有在 Done() 关闭后才有意义,可能的值是 context.Canceled 或 context.DeadlineExceeded。
  • Context 是并发安全的,可以被多个 goroutine 同时读取。

context.Background

作用

返回一个非 nil 的空 Context,永不取消、没有 deadline、没有 value。它是所有 context 树的根节点。

用法

ctx := context.Background()

典型场景

  • main 函数入口
  • 初始化、测试代码
  • 作为其他 context 的父节点
  • 当你不确定该用什么 context 时的”安全默认值”

示例

package main

import (
"context"
"fmt"
)

func main() {
ctx := context.Background()
fmt.Printf("%#v\n", ctx) // context.backgroundCtx{emptyCtx{...}}
fmt.Println(ctx.Done()) // <nil>
fmt.Println(ctx.Err()) // <nil>
fmt.Println(ctx.Value("any")) // <nil>
}

注意:Background() 返回的 ctx 的 Done() 是 nil,所以 select { case <-ctx.Done(): } 会永远阻塞。它本身不能被取消。


context.TODO

作用

同样返回一个非 nil 的空 Context,行为上和 Background() 完全等价(底层都是 emptyCtx),区别只在于语义:

  • Background:明确要把它当作根 context。
  • TODO:当前还不确定应该用什么 context,先占个位,将来会替换。

使用建议

  • 在重构中、还未确定上下文的地方用 TODO()。
  • 静态检查工具(如 staticcheck 的 SA1012)能识别出用 TODO 的地方,便于日后补全。
  • 不要用 nil 作为 context,需要占位时用 TODO()。

示例

func placeholder() {
// 暂未确定上下文来源,先占位,避免传 nil
ctx := context.TODO()
_ = ctx
}

与 Background 的区别表

维度 Background TODO
底层类型 emptyCtx emptyCtx
可取消 否 否
有 deadline 否 否
有 value 否 否
语义 根节点 待定占位
生产代码推荐 用于根 尽量避免

context.WithCancel

作用

基于父 context 派生一个可手动取消的子 context,返回 (ctx, cancel)。调用 cancel() 会:

  1. 关闭该 ctx 的 Done() channel;
  2. 让所有子 ctx 也一起被取消(取消具有向下传播的特性);
  3. 释放内部资源。

函数签名

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

关键要点

  • 必须调用 cancel(哪怕提前结束),否则会内存泄漏(父 ctx 一直持有对子 ctx 的引用,直到父被取消)。go vet 的 lostcancel 检查会捕捉这个问题。
  • 常见写法:defer cancel()。

典型场景

  • 用户主动中止操作(比如 HTTP handler 的请求结束)。
  • 一个任务失败后,取消其他并发任务(errgroup 的核心机制)。
  • 手动停止后台 goroutine。
  • 事件监听、循环中退出。

手动取消后台任务

package main

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

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 保险起见

go func() {
for {
select {
case <-ctx.Done():
fmt.Println("worker 收到取消信号:", ctx.Err())
return
case <-time.After(500 * time.Millisecond):
fmt.Println("worker 正在工作...")
}
}
}()

time.Sleep(2 * time.Second)
cancel() // 主动取消
time.Sleep(100 * time.Millisecond)
}

取消向下传播

func propagate() {
parent, cancelParent := context.WithCancel(context.Background())
defer cancelParent()

child, cancelChild := context.WithCancel(parent)
defer cancelChild()

cancelParent() // 父取消 → 子也被取消

<-child.Done()
fmt.Println("child 也被取消:", child.Err()) // context canceled
}

并发任务中一个出错,取消其余任务

func raceTasks() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

errCh := make(chan error, 2)
tasks := []func(context.Context) error{taskA, taskB}

for _, task := range tasks {
go func(f func(context.Context) error) {
if err := f(ctx); err != nil {
errCh <- err
cancel() // 任意任务失败,取消其它任务
}
}(task)
}

select {
case err := <-errCh:
return err
case <-ctx.Done():
return ctx.Err()
}
}

func taskA(ctx context.Context) error {
select {
case <-time.After(100 * time.Millisecond):
return errors.New("taskA failed")
case <-ctx.Done():
return ctx.Err()
}
}

func taskB(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

context.WithTimeout

作用

基于父 context 派生一个在指定时间后自动取消的子 context。返回 (ctx, cancel)。

函数签名

func WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

底层关系

// 等价于:
WithTimeout(parent, d) == WithDeadline(parent, time.Now().Add(d))

关键要点

  • timeout 是相对时间(从现在开始算多久后取消),容易阅读和修改。
  • 同样必须调用 cancel:即使超时自动取消,cancel 也要调用来释放 timer 资源。
  • 超时后 ctx.Err() 返回 context.DeadlineExceeded。

典型场景

  • HTTP 客户端调用外部服务设置最大等待时间。
  • 数据库查询超时。
  • 单次任务的耗时上限。
  • 用户请求的服务端超时控制(例如 API 网关必须 3s 内返回)。

模拟一次带超时的 HTTP 调用

func fetchData(ctx context.Context, url string) (string, error) {
// 模拟一个 3 秒才能完成的下游请求
select {
case <-time.After(3 * time.Second):
return "data from " + url, nil
case <-ctx.Done():
return "", ctx.Err()
}
}

func main() {
// 给这次请求 1 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

start := time.Now()
data, err := fetchData(ctx, "https://example.com/api")
fmt.Printf("耗时: %v, data: %q, err: %v\n",
time.Since(start), data, err)
// 耗时: ~1s, data: "", err: context deadline exceeded
}

context.WithDeadline

作用

与 WithTimeout 类似,但接收的是一个绝对时间点 time.Time,到该时间点时自动取消。

函数签名

func WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

与 WithTimeout 的差别

特性 WithTimeout WithDeadline
参数 相对时长 time.Duration 绝对时间 time.Time
可读性 简单直接,如 3*time.Second 需要先算好截止时刻
链式传播 父 deadline 更早时以父为准 同左
适用 单次操作的时长限制 多个操作共享同一个截止时刻,如”必须在 12:00 之前处理完”

重要:如果 父 ctx 已有的 deadline 比传入的更早,那么新 ctx 会继承父的更早 deadline,不会因为传入了更晚的时间而延迟取消。

典型场景

  • 一次会话/批处理的整体截止时刻(多个子请求共享这个 deadline)。
  • 多步流程(如 3 步依次调用下游),希望整体不超过某时间点。

多步骤流程共享截止时间

func step1(ctx context.Context) error {
select {
case <-time.After(400 * time.Millisecond):
fmt.Println("step1 完成")
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func step2(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
fmt.Println("step2 完成")
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func main() {
// 要求在 1 秒内跑完 step1 + step2
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

if err := step1(ctx); err != nil {
fmt.Println("step1 失败:", err)
return
}
if err := step2(ctx); err != nil {
fmt.Println("step2 失败:", err) // context deadline exceeded
return
}
}

注意:父 deadline 更早时优先

p, cancelP := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelP()

// 子 deadline 设成 5s 后,但父 1s 后就取消,实际 1s 就取消
c, cancelC := context.WithDeadline(p, time.Now().Add(5*time.Second))
defer cancelC()

<-c.Done()
fmt.Println(c.Err()) // context deadline exceeded(1s 后触发)

context.WithValue

作用

在 context 链上传递请求范围内的元数据,例如 traceID、用户 ID、认证 token、语言偏好等。

函数签名

func WithValue(parent Context, key, val any) Context

关键要点

  1. key 必须是可比较的(comparable),否则运行时 panic。
  2. 不要用内置类型(如 string)做 key——不同包之间的 "userID" 会冲突。推荐使用自定义的未导出类型。
  3. 只用于传递”请求作用域的元数据”,不要用来传递函数参数、业务可选参数。
  4. Value 查找是链式向上查找,线性开销,不要滥用。
  5. key 不能为 nil,否则 panic。

典型场景

  • 分布式追踪 traceID / spanID
  • 用户认证信息(注意安全性)
  • 请求 ID、语言、租户 ID
  • 中间件向下游函数传递元信息(如 HTTP middleware → handler → service)

不适用场景

  • 传递业务参数(应该用函数入参)
  • 存储大量数据或可变数据
  • 存储可选配置

用自定义类型作为 key

package main

import (
"context"
"fmt"
)

// 未导出类型作为 key,避免外部包冲突
type ctxKey int

const (
traceIDKey ctxKey = iota
userIDKey
)

func withTrace(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceIDKey, id)
}

func handle(ctx context.Context) {
if v := ctx.Value(traceIDKey); v != nil {
fmt.Println("traceID =", v.(string))
}
if v := ctx.Value(userIDKey); v != nil {
fmt.Println("userID =", v.(int))
}
}

func main() {
ctx := withTrace(context.Background(), "trace-abc-123")
ctx = context.WithValue(ctx, userIDKey, 42)
handle(ctx)
}

中间件传递 traceID

func TraceMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = uuid.NewString()
}
ctx := context.WithValue(r.Context(), traceIDKey{}, traceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func businessHandler(w http.ResponseWriter, r *http.Request) {
traceID, _ := r.Context().Value(traceIDKey{}).(string)
log.Printf("[trace=%s] handling request", traceID)
}

值会从父级继承

func inherited() {
parent := context.WithValue(context.Background(), traceIDKey, "trace-1")
child, cancel := context.WithTimeout(parent, time.Second)
defer cancel()

fmt.Println(child.Value(traceIDKey)) // trace-1(沿链找到)
}

带超时、取消传播、traceID 的任务调度

package main

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

type traceKey struct{}

// 模拟三个下游任务
func task1(ctx context.Context) error {
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("task1 ok, trace =", ctx.Value(traceKey{}))
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func task2(ctx context.Context) error {
select {
case <-time.After(200 * time.Millisecond):
return errors.New("task2: simulated failure")
case <-ctx.Done():
return ctx.Err()
}
}

func task3(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
fmt.Println("task3 被取消, trace =", ctx.Value(traceKey{}))
return ctx.Err()
}
}

func main() {
// 1) 根 context + traceID
ctx := context.WithValue(context.Background(), traceKey{}, "req-2024-0001")

// 2) 整体超时 3 秒
ctx, cancelTimeout := context.WithTimeout(ctx, 3*time.Second)
defer cancelTimeout()

// 3) 允许任务间相互取消
ctx, cancelAll := context.WithCancel(ctx)
defer cancelAll()

var wg sync.WaitGroup
errCh := make(chan error, 3)

tasks := []func(context.Context) error{task1, task2, task3}
for i, t := range tasks {
wg.Add(1)
go func(id int, f func(context.Context) error) {
defer wg.Done()
if err := f(ctx); err != nil {
errCh <- fmt.Errorf("task%d: %w", id, err)
cancelAll() // 一旦失败,通知其它任务立刻退出
}
}(i, t)
}

go func() {
wg.Wait()
close(errCh)
}()

for err := range errCh {
fmt.Println("收到错误:", err)
}

fmt.Println("所有任务结束,ctx.Err() =", ctx.Err())
}

运行结果预期:

task1 ok, trace = req-2024-0001
收到错误: task2: simulated failure
task3 被取消, trace = req-2024-0001
收到错误: task3: context canceled
所有任务结束,ctx.Err() = context canceled

这个小例子集中演示了:

  • WithValue 跨 goroutine 共享元数据;
  • WithTimeout 提供兜底的超时;
  • WithCancel 让”一错全错”的快速失败成为可能;
  • ctx.Done() 让阻塞操作及时退出,避免 goroutine 泄漏。

最佳实践与常见坑

最佳实践

  1. context 作为函数的第一个参数,命名通常为 ctx context.Context。
  2. 不要把 context 存到 struct 字段里(HTTP Request 里的除外)。
  3. cancel 一定要调用,最常用 defer cancel()。go vet 的 lostcancel 会检查。
  4. 不要传 nil,占位用 context.TODO()。
  5. WithValue 的 key 用未导出自定义类型,避免包间冲突。
  6. WithValue 只用于请求作用域元数据,不用于传业务参数。
  7. 父 ctx 的取消会传播到子,注意叶子端要及时 select 监听 ctx.Done()。
  8. 不要在 context 上保存可变数据;如果需要可变状态,用别的机制。

常见坑

坑 说明 规避
忘记调用 cancel 内存泄漏 / timer 泄漏 defer cancel()
把 nil 传进 WithCancel panic 用 Background/TODO
用 string 做 WithValue key 包间冲突 自定义未导出类型
在循环里 WithValue 存大量数据 内存膨胀 & 线性查找开销 只放少量、常量 key
ctx.Done() 返回 nil 时 select 永久阻塞 用会取消的 ctx,或用 default 处理
传 &ctx 或存到 struct 里 语义混乱、难以追踪生命周期 参数透传
WithDeadline 传入过去时间 立即取消 注意父 deadline 更早也会立即取消
把 context 当作通用”依赖注入”容器 可维护性差 用显式参数或专用结构体

Go 1.20+ 补充(延伸)

  • context.WithCancelCause / WithTimeoutCause / WithDeadlineCause:可以附带自定义取消原因,ctx.Err() 返回 Canceled,但可以通过 context.Cause(ctx) 拿到原始错误,非常适合追踪”到底为什么被取消”。
  • Go 1.21 引入的 context.AfterFunc(ctx, f):在 ctx 取消时执行一个回调函数,用于优雅清理(关闭连接、释放资源)。
  • Go 1.21 引入的 context.WithoutCancel(ctx):派生一个”不会被父取消影响”的 ctx,但保留 value,适合”即使父请求结束也要继续执行的后台任务”。

一张表总结

API 返回值 是否必须手动 cancel 主要用途 典型场景
Background() Context 否 根 context main、初始化
TODO() Context 否 占位 待定上下文
WithCancel(parent) (Context, CancelFunc) 是 手动取消 快速失败、主动中止、取消传播
WithTimeout(parent, d) (Context, CancelFunc) 是 相对时长超时 下游调用、DB 查询、API 超时
WithDeadline(parent, t) (Context, CancelFunc) 是 绝对截止时间 多步骤/会话共享截止时间
WithValue(parent, k, v) Context 否(由父 cancel 管) 传递请求元数据 traceID、userID、tenantID

掌握这六个 API 及其生命周期管理,就能在 Go 并发编程中正确处理”取消、超时、跨 goroutine 数据传递”这三类核心问题。

Context 接口四大方法详解:Deadline、Done、Err、Value

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}

这四个方法是所有 context 能力的”出口”:Deadline 看时限、Done 等信号、Err 查原因、Value 取数据。理解它们之间的配合关系,才是真正会用 context。


先建立整体认知

方法 返回值 触发时机 是否阻塞 主要用途
Deadline() (time.Time, bool) 创建时即可读 否 查询截止时刻,做提前判断/优化
Done() <-chan struct{} 取消/超时时被 close 阻塞(配 select) 等待取消信号
Err() error Done 关闭后才有值 否 区分”被取消”还是”超时”
Value(key) any 创建时即可读 否 读取请求作用域元数据

它们的关系可以概括为:

                  ┌──────────────┐
创建 context ──▶ │ ctx │
└──────┬───────┘
│
┌────────────────────┼─────────────────────┐
▼ ▼ ▼
Deadline() Done() Value()
查询截止时间 等待取消信号 读取元数据
│
▼ (Done 被 close 之后)
Err()
判断取消原因

关键理解:

  • Done() 是通知机制(channel close)。
  • Err() 是 Done() 的原因说明,必须在 Done() 关闭后才调用才有意义。
  • Deadline() 是提前查询,让你在阻塞前就能做决策。
  • Value() 是旁路数据通道,与取消机制完全独立。

下面逐一拆解。


Deadline() —— 查询截止时刻

作用与语义

Deadline() (deadline time.Time, ok bool)
  • 返回该 context 的截止时间。
  • 如果 context 没有设置截止时间,返回 (time.Time{}, false),即零值和 false。
  • Background()、TODO()、WithCancel()、WithValue() 返回的 ctx 都没有 deadline,ok=false。
  • WithTimeout、WithDeadline 返回的 ctx 有 deadline,ok=true。
  • 子 context 会继承父 context 的 deadline,且如果父的 deadline 更早,子以自己的为准(取更早的)。

典型应用场景

场景 用法
发起下游请求前判断剩余时间是否足够 if remain < minNeeded { return err }
动态设置子操作超时 根据剩余时间给每个子请求分配 budget
批处理/循环中避免做无用功 剩余时间不够就提前退出
日志/监控中记录预算 打印剩余时间做观测

注意:Deadline 只是一个”查询”,它本身不会阻塞也不会通知。真正的取消信号来自 Done()。

查询是否有 deadline,以及剩余时间

package main

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

func inspectDeadline(name string, ctx context.Context) {
deadline, ok := ctx.Deadline()
if !ok {
fmt.Printf("[%s] 没有设置 deadline\n", name)
return
}
fmt.Printf("[%s] deadline = %v, 剩余 = %v\n",
name,
deadline.Format(time.RFC3339),
time.Until(deadline).Round(time.Millisecond),
)
}

func main() {
inspectDeadline("Background", context.Background()) // 没有设置 deadline
inspectDeadline("WithCancel", mustCancel(context.Background())) // 没有设置 deadline

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
inspectDeadline("WithTimeout", ctx) // 有,剩余 ~2s
}

func mustCancel(p context.Context) context.Context {
c, cancel := context.WithCancel(p)
_ = cancel
return c
}

输出:

[Background] 没有设置 deadline
[WithCancel] 没有设置 deadline
[WithTimeout] deadline = 2026-10-21T..., 剩余 = 2s

根据剩余时间决定是否发起下游请求

这是 Deadline() 最有价值的用法——避免明知会超时还硬发请求。

func callDownstream(ctx context.Context, url string) error {
// 假设这个请求平均需要 800ms 才能完成
const estimatedCost = 800 * time.Millisecond

if deadline, ok := ctx.Deadline(); ok {
remaining := time.Until(deadline)
if remaining < estimatedCost {
return fmt.Errorf("剩余时间 %v 不足以发起请求(需要 %v)",
remaining.Round(time.Millisecond), estimatedCost)
}
}

// ... 真正发起 HTTP 请求
return nil
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

if err := callDownstream(ctx, "https://api.example.com"); err != nil {
fmt.Println("跳过请求:", err)
// 跳过请求: 剩余时间 500ms 不足以发起请求(需要 800ms)
}
}

多步骤批处理时动态分配剩余预算

func processBatch(ctx context.Context, items []string) {
for i, item := range items {
deadline, ok := ctx.Deadline()
if !ok {
// 没有 deadline,直接处理
process(item)
continue
}

remaining := time.Until(deadline)
remainingItems := len(items) - i
// 剩余时间不足以平均分给剩余元素时,提前退出
if remaining < time.Duration(remainingItems)*100*time.Millisecond {
fmt.Printf("剩余时间不足,处理完 %d/%d 项后提前退出\n", i, len(items))
return
}
process(item)
}
}

坑与注意

  • 不要在 ok=false 时使用返回的 time.Time,零值的时间是 0001-01-01,容易造成错误判断。
  • time.Until(deadline) 可能为负(已超时),要处理这个情况。
  • Deadline 只描述”未来某个时刻会取消”,但 context 也可能被提前手动取消(父 cancel() 被调用)。所以不能只靠 Deadline 判断是否还被需要——真正的取消信号永远看 Done()。

Done() —— 等待取消信号(核心)

作用与语义

Done() <-chan struct{}
  • 返回一个只读 channel。
  • 当 context 被取消(手动 cancel 或超时)时,该 channel 会被 close。
  • 只是 close,不会发送任何值。所以读取它永远拿到零值 struct{}{}。
  • 未取消前,从它读取会阻塞;取消后,从它读取会立即返回。
  • Background()、TODO() 返回的 ctx 的 Done() 是 nil,意味着永远不取消。在 select 中监听 nil channel 会永久阻塞该 case。

为什么用 channel close 而不是发值

  • close 可以被”广播”:无论多少个 goroutine 在监听,全部同时被唤醒。
  • 不需要消费:不会因为没人在读而阻塞发送者。
  • 接收零值语义清晰:<-ch 拿到 struct{}{},配合 select 简单明了。

标准用法范式

select {
case <-ctx.Done():
return ctx.Err() // 或被取消时的处理
case result := <-workCh:
return result, nil
case <-time.After(d):
return nil, errTimeout
}

典型应用场景

场景 说明
可中断的阻塞操作 HTTP 请求、DB 查询、time.Sleep 替代、channel 等待
后台 worker 退出 循环中监听 Done(),收到信号就退出
快速失败 并发任务中任意一个失败就取消其它任务
超时控制 配合 WithTimeout,到点自动退出
优雅停机 服务收到停止信号时通知所有 handler 结束

把不可中断的操作变成可中断

time.Sleep 无法被中断,用它做”可中断等待”用 select + ctx.Done() 替换:

// 不可中断,即使 ctx 取消也要等满 5s
func badWait(ctx context.Context) {
time.Sleep(5 * time.Second)
}

// 可中断,ctx 一取消就返回
func goodWait(ctx context.Context, d time.Duration) error {
select {
case <-time.After(d):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

start := time.Now()
err := goodWait(ctx, 5*time.Second)
fmt.Printf("耗时 %v, err = %v\n", time.Since(start).Round(time.Millisecond), err)
// 耗时 1s, err = context deadline exceeded
}

后台 worker 的退出

func worker(ctx context.Context, id int, jobs <-chan int) {
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d 收到停止信号: %v\n", id, ctx.Err())
return
case job, ok := <-jobs:
if !ok {
fmt.Printf("worker %d: jobs 已关闭\n", id)
return
}
fmt.Printf("worker %d 处理 job %d\n", id, job)
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())

jobs := make(chan int, 10)
for i := 1; i <= 3; i++ {
go worker(ctx, i, jobs)
}

for i := 1; i <= 5; i++ {
jobs <- i
}

time.Sleep(500 * time.Millisecond)
cancel() // 通知所有 worker 退出
time.Sleep(200 * time.Millisecond)
}

多个 goroutine 同时被一个 Done 唤醒(广播语义)

func main() {
ctx, cancel := context.WithCancel(context.Background())

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
<-ctx.Done() // 阻塞等待
fmt.Printf("goroutine %d 被唤醒\n", id)
}(i)
}

time.Sleep(100 * time.Millisecond)
cancel() // 一个 close,5 个 goroutine 全部醒来
wg.Wait()
}

输出:

goroutine 0 被唤醒
goroutine 3 被唤醒
goroutine 1 被唤醒
goroutine 4 被唤醒
goroutine 2 被唤醒

这就是 close 的广播能力——cancel() 只需要 close 一次,所有监听者都能感知。

Done() 为 nil 的坑

func badSelect() {
ctx := context.Background()
// ctx.Done() == nil,下面的 case 永远阻塞
select {
case <-ctx.Done():
fmt.Println("永远不会执行")
}
// 整个 goroutine 永久阻塞!
}

func fixSelect() {
ctx := context.Background()
select {
case <-ctx.Done():
fmt.Println("取消")
case <-time.After(100 * time.Millisecond):
fmt.Println("超时兜底") // 会被执行
}
}

结论:如果一个 ctx 可能返回 nil 的 Done(),要么换成 WithCancel/WithTimeout 等非空的 context,要么在 select 里加上 default 或超时分支。

3.9 与 Deadline() 的关系

  • Deadline() 让你提前知道未来会取消。
  • Done() 让你真正被通知取消。
  • 有 deadline 的 ctx 到点后,Done() 会自动被 close,然后 Err() 返回 DeadlineExceeded。

Err() —— 查询取消原因

作用与语义

Err() error
  • Done() 尚未关闭时,返回 nil。
  • Done() 关闭后,返回具体的取消原因:
    • context.Canceled:被手动 cancel() 取消(或因父 ctx 被取消传播)。
    • context.DeadlineExceeded:超时导致取消。
    • (Go 1.20+ 通过 WithCancelCause 派生的 ctx,Err() 仍返回上面两个之一,但 context.Cause(ctx) 可以拿到自定义原因。)

为什么要用 Err() 而不是直接判断错误

  • ctx.Done() 只能告诉你”取消了”,不能区分是主动取消还是超时。
  • 很多业务逻辑需要对超时和主动取消做不同处理:
    • 超时 → 记录慢请求、告警、重试。
    • 主动取消 → 客户端断开,通常不必告警、直接退出即可。
  • Err() 提供标准化的错误值,可以用 errors.Is 判断。

典型应用场景

场景 用法
区分”超时”和”用户取消”做不同处理 errors.Is(ctx.Err(), context.DeadlineExceeded)
日志/监控分类 记录取消原因
向上层返回错误 return ctx.Err() 让调用方判断
判断是否值得重试 超时也许能重试,主动取消通常不重试

区分取消原因做不同处理

func doWork(ctx context.Context) error {
select {
case <-time.After(3 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func handleError(ctx context.Context, err error) {
switch {
case errors.Is(err, context.Canceled):
fmt.Println("❌ 请求被主动取消(可能是客户端断开)")
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("⏰ 请求超时(可能是下游太慢,考虑告警/重试)")
case err == nil:
fmt.Println("✅ 成功")
default:
fmt.Printf("⚠️ 其他错误: %v\n", err)
}
}

func main() {
// 超时场景
ctx1, cancel1 := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel1()
handleError(ctx1, doWork(ctx1))

// 主动取消场景
ctx2, cancel2 := context.WithCancel(context.Background())
go func() {
time.Sleep(200 * time.Millisecond)
cancel2()
}()
handleError(ctx2, doWork(ctx2))
}

输出:

⏰ 请求超时(可能是下游太慢,考虑告警/重试)
❌ 请求被主动取消(可能是客户端断开)

错误的正确包装与向上传递

func getUser(ctx context.Context, id int) (*User, error) {
user, err := dbQuery(ctx, id)
if err != nil {
// 不要丢弃 ctx.Err(),用 %w 包装保留原始错误链
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("getUser(%d): %w", id, context.DeadlineExceeded)
}
return nil, fmt.Errorf("getUser(%d): %w", id, err)
}
return user, nil
}

// 调用方可以继续用 errors.Is 判断
func handler(ctx context.Context) {
_, err := getUser(ctx, 42)
if errors.Is(err, context.DeadlineExceeded) {
// 记录慢请求、触发重试等
}
}

Err() 与 Done() 的时序

func checkTiming() {
ctx, cancel := context.WithCancel(context.Background())

fmt.Println("取消前:", ctx.Err()) // nil

go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()

<-ctx.Done() // 等 Done 被 close
fmt.Println("取消后:", ctx.Err()) // context canceled
}

规则:Err() 只有在 Done() 关闭后才可能非 nil。所以典型的写法是先 <-ctx.Done() 再读 ctx.Err(),或者直接 select 的 case <-ctx.Done(): return ctx.Err()。

Go 1.20+ 自定义取消原因(延伸)

func advancedCancel() {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("用户余额不足")) // 传入原因

<-ctx.Done()
fmt.Println("ctx.Err() =", ctx.Err()) // context canceled
fmt.Println("context.Cause(ctx) =", context.Cause(ctx)) // 用户余额不足
}

注意:Err() 永远只返回 Canceled / DeadlineExceeded,自定义原因要读 context.Cause()。这是 Go 1.20 之后排查问题的新利器。


Value() —— 读取请求作用域元数据

作用与语义

Value(key any) any
  • 沿着 context 链向上查找 key 对应的 value。
  • 找到最近一层的 value 就返回;找不到返回 nil。
  • 线性查找(通过链上的 map 逐层找),性能 O(层数)。
  • key 必须可比较(comparable),且不能为 nil,否则 panic。
  • 返回类型是 any,需要做类型断言。

核心原则(务必牢记)

  1. key 用未导出的自定义类型,避免跨包冲突。
    • ❌ context.WithValue(ctx, "userID", 42)
    • ✅ context.WithValue(ctx, userIDKey{}, 42)
  2. 只放请求作用域元数据,如 traceID、userID、tenantID、语言偏好。
  3. 不要放业务参数(应该作为函数参数显式传递)。
  4. 不要放可变数据(并发安全问题)。
  5. 不要放大量数据(线性查找 + 内存膨胀)。

典型应用场景

场景 说明
分布式追踪 traceID / spanID,跨 goroutine 传递
认证信息 userID、角色、token(注意安全)
多租户 tenantID
请求级别配置 语言、时区、feature flag
中间件向下游透传 HTTP middleware → handler → service → repo

基础用法 + 类型安全封装

package main

import (
"context"
"fmt"
)

// 未导出类型 + 常量,杜绝 key 冲突
type ctxKey int

const (
traceIDKey ctxKey = iota
userIDKey
)

// 推荐封装成 setter/getter,暴露类型安全的 API
func WithTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceIDKey, id)
}

func TraceIDFrom(ctx context.Context) (string, bool) {
v, ok := ctx.Value(traceIDKey).(string)
return v, ok
}

func WithUserID(ctx context.Context, id int64) context.Context {
return context.WithValue(ctx, userIDKey, id)
}

func UserIDFrom(ctx context.Context) (int64, bool) {
v, ok := ctx.Value(userIDKey).(int64)
return v, ok
}

func main() {
ctx := context.Background()
ctx = WithTraceID(ctx, "trace-abc-123")
ctx = WithUserID(ctx, 42)

if traceID, ok := TraceIDFrom(ctx); ok {
fmt.Println("traceID =", traceID)
}
if userID, ok := UserIDFrom(ctx); ok {
fmt.Println("userID =", userID)
}

// 未设置的 key 返回 nil
fmt.Println("未设置的 key 断言:", ctx.Value("missing"))
}

链式继承 —— 子 ctx 能读到父的 value

func chainValue() {
root := context.WithValue(context.Background(), traceIDKey, "trace-001")
withTimeout, cancel := context.WithTimeout(root, time.Second)
defer cancel()

// 子 ctx 沿链向上找到父的 value
fmt.Println("child 读到:", withTimeout.Value(traceIDKey)) // trace-001

// 覆盖父的同名 key,就近生效
child := context.WithValue(withTimeout, traceIDKey, "trace-002")
fmt.Println("child 覆盖后:", child.Value(traceIDKey)) // trace-002
fmt.Println("父仍然原值:", withTimeout.Value(traceIDKey)) // trace-001
}

要点:

  • WithValue 不会修改原 ctx,而是返回一个新的 ctx,带一层额外映射。
  • 查找时从近到远,最近的有效。

模拟 HTTP 中间件透传 traceID

type traceIDKeyType struct{} // 匿名 struct 做 key 的常见写法

func TraceMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = generateTraceID()
}
ctx := context.WithValue(r.Context(), traceIDKeyType{}, traceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func BusinessHandler(w http.ResponseWriter, r *http.Request) {
traceID, _ := r.Context().Value(traceIDKeyType{}).(string)
log.Printf("[trace=%s] handling request", traceID)

// 下游 DB 查询也带 traceID
if err := queryDB(r.Context()); err != nil {
// ...
}
}

不要滥用 Value —— 反模式对比

// ❌ 反模式:用 ctx 传递业务参数
func BadCreateUser(ctx context.Context) error {
name := ctx.Value("name").(string) // 类型断言易 panic
age := ctx.Value("age").(int)
return saveUser(name, age)
}

// ✅ 正确:业务参数用函数入参,ctx 只传元数据
func GoodCreateUser(ctx context.Context, name string, age int) error {
traceID, _ := ctx.Value(traceIDKey{}).(string) // 元数据走 ctx
log.Printf("[trace=%s] create user name=%s age=%d", traceID, name, age)
return saveUser(name, age)
}

Value 的性能与浅坑

func benchmarkValue() {
// 每层 WithValue 都是一次映射开销 + 查找时的线性开销
ctx := context.Background()
for i := 0; i < 1000; i++ {
ctx = context.WithValue(ctx, ctxKey(i), i)
}
_ = ctx.Value(ctxKey(999)) // 线性查找 1000 层
}

建议:

  • 元数据控制在个位数。
  • 频繁读取的 value 可在 handler 入口一次取出放入局部变量,避免反复 Value() 查找。

类型断言的防御式写法

// 不安全的类型断言(key 存在但类型不对会 panic)
// v := ctx.Value(traceIDKey).(string)

// 安全的逗号-ok 断言
if v, ok := ctx.Value(traceIDKey).(string); ok && v != "" {
// 使用 v
}

Value() 返回 nil 时若直接 .(string) 会 panic,务必用 , ok 形式。


四方法协同:完整示例

下面用一个完整的服务调用链把四个方法都用上:

package main

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

type traceKey struct{}

// ---- 下游调用:会用到 Deadline 判断 + Done 等待 + Err 分类 ----
func callDownstream(ctx context.Context) (string, error) {
traceID, _ := ctx.Value(traceKey{}).(string)

// ① Deadline:剩余时间不够就直接放弃
if deadline, ok := ctx.Deadline(); ok {
if time.Until(deadline) < 300*time.Millisecond {
return "", fmt.Errorf("[trace=%s] 剩余时间不足,跳过下游调用", traceID)
}
}

// ② Done:可中断的阻塞等待
select {
case <-time.After(1 * time.Second): // 模拟下游耗时 1s
return fmt.Sprintf("[trace=%s] downstream ok", traceID), nil
case <-ctx.Done():
// ③ Err:区分取消原因
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return "", fmt.Errorf("[trace=%s] 下游调用超时: %w", traceID, ctx.Err())
}
return "", fmt.Errorf("[trace=%s] 下游调用被取消: %w", traceID, ctx.Err())
}
}

// ---- 主流程 ----
func handleRequest(ctx context.Context) (string, error) {
traceID, _ := ctx.Value(traceKey{}).(string) // ④ Value:读取元数据
fmt.Printf("[trace=%s] 开始处理\n", traceID)
return callDownstream(ctx)
}

func main() {
// 场景 A:正常
func() {
ctx := context.WithValue(context.Background(), traceKey{}, "REQ-A")
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()

result, err := handleRequest(ctx)
fmt.Printf("A 结果: %q, err=%v\n\n", result, err)
}()

// 场景 B:超时(下游 1s,但整体只给 500ms,且剩余 500ms>300ms,进入 select 后超时)
func() {
ctx := context.WithValue(context.Background(), traceKey{}, "REQ-B")
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()

result, err := handleRequest(ctx)
fmt.Printf("B 结果: %q, err=%v\n\n", result, err)
}()

// 场景 C:剩余时间不足(deadline 只剩 100ms),Deadline 判断直接跳过
func() {
ctx := context.WithValue(context.Background(), traceKey{}, "REQ-C")
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
defer cancel()

result, err := handleRequest(ctx)
fmt.Printf("C 结果: %q, err=%v\n\n", result, err)
}()

// 场景 D:主动取消
func() {
ctx := context.WithValue(context.Background(), traceKey{}, "REQ-D")
ctx, cancel := context.WithCancel(ctx)
go func() {
time.Sleep(200 * time.Millisecond)
cancel()
}()

result, err := handleRequest(ctx)
fmt.Printf("D 结果: %q, err=%v\n", result, err)
}()
}

输出(示意):

[trace=REQ-A] 开始处理
A 结果: "[trace=REQ-A] downstream ok", err=<nil>

[trace=REQ-B] 开始处理
B 结果: "", err=[trace=REQ-B] 下游调用超时: context deadline exceeded

[trace=REQ-C] 开始处理
C 结果: "", err=[trace=REQ-C] 剩余时间不足,跳过下游调用

[trace=REQ-D] 开始处理
D 结果: "", err=[trace=REQ-D] 下游调用被取消: context canceled

这段代码完整演示了:

  • Value 传递 traceID,贯穿整个调用链;
  • Deadline 提前判断预算,避免无效请求;
  • Done 让阻塞的等待可以被立即中断;
  • Err 区分了”超时”与”主动取消”,支持不同错误处理策略。

常见坑速查

Deadline 相关

坑 规避
ok=false 时使用了零值时间 先判断 ok
以为 Deadline() 会阻塞 它只是查询,阻塞请用 Done()
只靠 Deadline 判断是否被取消 手动 cancel() 也会取消,要监听 Done()

Done 相关

坑 规避
监听 nil channel 永久阻塞 用非空 ctx 或加 default/超时兜底
用 <-ctx.Done() 而不用 select 做多路等待 用 select 同时监听其他 channel
在 Done() 上读取值 close 不发值,读到的是零值

Err 相关

坑 规避
Done 未关闭时就读 Err() 先 <-ctx.Done()
用字符串比较错误 用 errors.Is(err, context.Canceled)
忘记包装导致错误链断掉 fmt.Errorf("...: %w", ctx.Err())
需要自定义原因却只看 Err() Go 1.20+ 用 context.Cause(ctx)

Value 相关

坑 规避
用 string/int 做 key 自定义未导出类型(type ctxKey int 或 struct{})
key 为 nil 永远不要传 nil
直接类型断言 用 v, ok := ctx.Value(k).(T)
用 Value 传业务参数 业务参数用函数入参
放大量数据 只放少量元数据
每层反复取 value handler 入口取一次存局部变量

总结对照表

方法 何时读 是否需要阻塞 关键点 一句话用途
Deadline() 发起操作前 否 返回 (时间, ok),父 deadline 会传播 提前决策:剩余时间够不够
Done() 等待时 是(配 select) 只 close 不发值;nil 会永久阻塞 等待取消:可中断阻塞
Err() Done() 关闭后 否 返回 Canceled/DeadlineExceeded 查询原因:区分取消类型
Value(key) 任意时刻 否 链式查找;key 用未导出类型 读取元数据:traceID、userID

记住一条主线:

创建时用 WithXxx 塑造取消行为 → 使用时用 Deadline() 做提前判断,用 Done() 阻塞等待取消,用 Err() 区分原因,用 Value() 旁路传递元数据。

至此,Context 接口的创建侧(Background/TODO/WithCancel/WithTimeout/WithDeadline/WithValue)和消费侧(Deadline/Done/Err/Value)就完整闭环了。日常开发中绝大多数并发控制问题,都能用这一套模型清晰表达。