第 24 章:上下文管理——context 包

第 24 章:上下文管理——context 包

想象一个场景:你点了一份外卖,骑手正在路上,但突然你不想要了——这时候你需要一种机制,能够让骑手停止配送。在 Go 的世界里,context 包就是那个「外卖取消按钮」,让你能够在合适的时机优雅地停止正在进行的操作。

context 包是 Go 语言中最容易被低估的包之一。它不仅仅是一个传递数据的容器,更是一套完善的取消信号传播机制。掌握它,你就能写出更健壮、更可控的 Go 程序。


24.1 context 包解决什么问题

在真实的系统开发中,我们经常会遇到这样的困境:

  • Web 请求可能超时需要取消:用户发起了一个请求,后端需要调用多个微服务,但如果用户等不及了关闭了页面,这些正在进行的调用应该被取消,否则就是浪费资源。
  • 数据库查询可能需要截止时间:一个 SQL 查询如果超过 5 秒还没返回结果,大概率是出问题了,应该果断放弃。
  • 请求 ID 需要传递:一个请求从网关到微服务到数据库,日志里需要串联起整个链路,这就需要传递请求级别的元数据。

如果没有 context,我们可能需要:

  1. 在每个函数里加一个 cancel 频道参数
  2. 在每个层级手动传递 deadline
  3. 用全局 map 存储请求级别的数据

这样做的结果是:每个函数签名都变得又臭又长,而且取消逻辑散落得到处都是

context 包就是来解决这个问题的——它提供了一种统一的方式来传递取消信号、截止时间和请求级别的数据,而且这一切都可以通过一个简单的 context.Context 接口来完成。

1
2
3
4
5
6
7
8
9
// 没有 context 的世界,每个函数都要加一堆参数
func QueryUser(userID string, cancel chan struct{}, deadline time.Time, requestID string) {
    // 参数列表长得让人窒息
}

// 有 context 的世界,一切都很优雅
func QueryUser(ctx context.Context, userID string) {
    // 简洁、清晰、可组合
}

核心概念context 包的核心思想是将取消信号和元数据绑定到一个「上下文」对象上,然后让这个对象在调用链中流动。每个函数都能从这个对象中获取自己需要的信息,同时也能感知到「外面」是否要求取消。


24.2 context 核心原理:Context 接口

context.Context 是整个 context 包的核心,它是一个接口,定义了四个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Context interface {
    // Deadline 返回这个 Context 的截止时间
    // 如果没有设置截止时间,ok 返回 false
    Deadline() (deadline time.Time, ok bool)

    // Done 返回一个关闭的频道,当 Context 被取消时会关闭
    // 如果这个 Context 永远不会被取消,返回 nil
    Done() <-chan struct{}

    // Err 返回 Context 被取消的原因
    // 如果 Done() 还没关闭,返回 nil
    Err() error

    // Value 返回这个 Context 中存储的值
    // 如果没有这个值,返回 nil
    Value(key any) any
}

方法详解

方法返回值作用
Deadline()(time.Time, bool)返回截止时间,bool 表示是否设置了截止时间
Done()<-chan struct{}返回一个频道,当 Context 被取消时关闭
Err()error返回取消原因:CanceledDeadlineExceeded
Value()any根据 key 查询存储的值

接口方法对应的 Mermaid 图

graph TD
    subgraph "context.Context 接口"
        D["Deadline() time.Time, bool"]
        O["Done() <-chan struct{}"]
        E["Err() error"]
        V["Value(key) any"]
    end

    D -->|"截止时间"| T["返回截止时间"]
    O -->|"取消信号"| S["关闭时表示取消"]
    E -->|"取消原因"| C["Canceled 或 DeadlineExceeded"]
    V -->|"查询值"| K["沿着 Context 链查找"]

为什么 Done() 返回的是 <-chan struct{} 而不是 <-chan bool>

因为 struct{} 是 Go 中最节省内存的类型,不占用任何实际空间(0 字节)。用它来做信号量再合适不过了——我们只关心频道是否关闭,不关心里面有什么值。


24.3 context.Background:创建根 Context

context.Background() 是最常用的创建根 Context 的方式。它是一个空的、永远不会被取消的 Context

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    // context.Background() 返回一个 nil 错误的 Context
    // 它是整个 Context 树的根,通常作为请求的入口点
    ctx := context.Background()

    fmt.Println("Context:", ctx)
    fmt.Println("Deadline:", ctx.Deadline())
    // 输出: Deadline: 0001-01-01 00:00:00 +0000 UTC, false
    fmt.Println("Done 通道:", ctx.Done())  // 输出: nil(永远不会被取消)
    fmt.Println("Err:", ctx.Err())  // 输出: nil
}

什么时候用 Background?

  • 程序的入口点(如 main 函数)
  • 最高层的请求处理器
  • 单元测试
  • 任何需要作为根节点的地方

记住:Background 永远不会取消,你可以在它基础上派生出可取消的子 Context。


24.4 context.TODO:临时占位 Context

context.TODO()context.Background() 长得几乎一模一样,但用途完全不同。顾名思义,TODO 就是「还没想好用什么 Context」的占位符

1
2
3
4
5
6
7
8
func main() {
    // TODO 和 Background 在功能上几乎一样
    // 但语义不同:TODO 表示"将来会替换成合适的 Context"
    ctx := context.TODO()

    fmt.Println("Context:", ctx)
    fmt.Println("Done 通道:", ctx.Done())
}

使用场景

1
2
3
4
5
6
7
8
// 假设这个函数还没确定如何获取 Context
func processData(data string) error {
    // 这里还不知道该用 Background 还是从参数传入
    // 所以先用 TODO 占位,后续重构时会改成正确的 Context
    ctx := context.TODO()

    return fetchFromDB(ctx, data)
}

最佳实践:在代码中搜索 TODO(),确保它们最终都被替换成正确的 Context 来源。如果代码里有太多 TODO(),说明你的架构设计还需要改进。


24.5 context.WithCancel:创建可取消 Context

context.WithCancel(parent) 从父 Context 创建一个可取消的子 Context。当你调用返回的 cancel 函数时,这个子 Context 就会被取消。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
    // 从 Background 创建可取消 Context
    ctx, cancel := context.WithCancel(context.Background())

    // 在另一个 goroutine 中模拟工作
    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("工作被取消,退出")
                return
            default:
                fmt.Println("工作中...")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()

    // 主函数等待 2 秒后取消
    time.Sleep(2 * time.Second)
    fmt.Println("调用取消函数")
    cancel()

    // 等待一下让 goroutine 收到信号
    time.Sleep(500 * time.Millisecond)
}

输出

工作中...
工作中...
工作中...
工作中...
工作中...
工作中...
工作中...
工作中...
调用取消函数
工作被取消,退出

WithCancel 的 Mermaid 图

graph TD
    A["context.Background()"] --> B["context.WithCancel(parent)"]
    B --> C["ctx(子 Context)"]
    B --> D["cancel() 函数"]

    C -->|"取消时"| E["ctx.Done() 关闭"]
    D -->|"调用后"| F["子 Context 被取消"]

cancel 函数:一个 cancel 函数可以取消多个 Context。合理使用可以减少资源泄漏。


24.6 context.WithDeadline:创建带截止时间的 Context

context.WithDeadline(parent, time) 创建一个在指定时间自动取消的 Context。当系统时间到达截止时间时,Context 会自动取消。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
    // 创建一个 3 秒后截止的 Context
    deadline := time.Now().Add(3 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()  // 别忘了清理资源

    fmt.Printf("当前时间: %v\n", time.Now().Format("15:04:05.000"))
    fmt.Printf("截止时间: %v\n", deadline.Format("15:04:05.000"))

    i := 0
    for {
        i++
        select {
        case <-ctx.Done():
            fmt.Printf("Context 已取消: %v(第 %d 次循环后)\n", ctx.Err(), i)
            return
        default:
            fmt.Printf("执行任务 %d...\n", i)
            time.Sleep(1 * time.Second)
        }
    }
}

输出

当前时间: 14:30:00.000
截止时间: 14:30:03.000
执行任务 1...
执行任务 2...
执行任务 3...
Context 已取消: context deadline exceeded(第 3 次循环后)

适用场景

  • 数据库查询的最长时间限制
  • 外部 API 调用的超时
  • 批处理任务的整体时间限制

24.7 context.WithTimeout:创建带超时的 Context

context.WithTimeout(parent, duration) 实际上是 WithDeadline 的语法糖。它接受一个时间间隔而不是绝对时间,内部会自动计算截止时间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
    // 创建一个 5 秒超时的 Context
    // WithTimeout(parent, duration) 等价于 WithDeadline(parent, time.Now().Add(duration))
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    fmt.Println("超时 Context 已创建,5 秒后将自动取消")

    for {
        select {
        case <-ctx.Done():
            fmt.Printf("超时触发: %v\n", ctx.Err())
            return
        default:
            fmt.Println("处理中...")
            time.Sleep(1 * time.Second)
        }
    }
}

WithTimeout vs WithDeadline 选择指南

函数参数类型适用场景
WithTimeouttime.Duration相对时间,如「5 秒后」
WithDeadlinetime.Time绝对时间,如「下午 3 点」

实际使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// HTTP 服务中设置请求超时
func handler(w http.ResponseWriter, r *http.Request) {
    // 创建 10 秒超时的 Context
    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
    defer cancel()

    // 将 Context 传递给实际处理逻辑
    result, err := doHeavyWork(ctx)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }

    w.Write(result)
}

24.8 context.WithCancelCause(Go 1.21+):支持传递取消原因的 WithCancel

Go 1.21 引入了 context.WithCancelCauseCause() 方法,这让取消原因的传递变得轻而易举。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    ctx, cancel := context.WithCancelCause(context.Background())

    go func() {
        // 模拟一个操作失败
        time.Sleep(2 * time.Second)
        // 传递具体的取消原因
        cancel(errors.New("数据库连接失败:connection refused"))
    }()

    <-ctx.Done()

    // Cause() 返回传递给 cancel 的具体错误
    fmt.Printf("取消原因: %v\n", context.Cause(ctx))
}

输出

取消原因: 数据库连接失败:connection refused

WithCancel vs WithCancelCause 对比

1
2
3
4
5
6
7
8
9
// WithCancel:只能知道「被取消了」,不知道为什么
ctx1, cancel1 := context.WithCancel(context.Background())
// ...
cancel1()  // 只能取消,无法传递原因

// WithCancelCause:不仅能取消,还能传递一个错误作为原因
ctx2, cancel2 := context.WithCancelCause(context.Background())
// ...
cancel2(errors.New("操作失败的具体原因"))  // 取消并附带原因

使用场景:当你不只是要取消一个操作,还需要知道为什么取消的时候,用 WithCancelCause


24.9 context.WithValue:创建带值的 Context

context.WithValue(parent, key, value) 创建一个携带键值对的子 Context。子 Context 可以从父 Context 继承值,也可以覆盖父 Context 的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义一个非导出的 key 类型(这是最佳实践,后面会详细讲)
type requestIDKey struct{}

func main() {
    ctx := context.Background()

    // 存储请求 ID
    ctx = context.WithValue(ctx, requestIDKey{}, "req-12345")

    // 在子 Context 中存储用户 ID
    childCtx := context.WithValue(ctx, "userID", 10086)

    // 读取值
    reqID := childCtx.Value(requestIDKey{})
    userID := childCtx.Value("userID")

    fmt.Printf("请求 ID: %v\n", reqID)
    fmt.Printf("用户 ID: %v\n", userID)

    // 从父 Context 也能读取到值
    fmt.Printf("从父 Context 读取请求 ID: %v\n", ctx.Value(requestIDKey{}))
}

输出

请求 ID: req-12345
用户 ID: 10086
从父 Context 读取请求 ID: req-12345

Value 查找的 Mermaid 图

graph TD
    subgraph "Context 链"
        A["ctx (Background)"]
        A -->|WithValue| B["ctx + requestID"]
        B -->|WithValue| C["childCtx + userID"]
    end

    C -->|"Value(userID)"| D["直接在当前 Context 找到: 10086"]
    C -->|"Value(requestIDKey)"| E["沿着链向上查找"]
    E -->|"在 B 中找到"| F["req-12345"]
    E -->|"继续向上"| G["在 A 中找,没找到"]
    G -->|"返回 nil"| H["nil"]

24.10 Context.Done():返回取消信号频道

Done() 方法返回一个只读的频道。当 Context 被取消(主动取消或超时)时,这个频道会被关闭。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func simulateTask(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("[%s] 收到取消信号,退出: %v\n", name, ctx.Err())
            return
        default:
            fmt.Printf("[%s] 工作中...\n", name)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

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

    go simulateTask(ctx, "任务A")
    go simulateTask(ctx, "任务B")

    <-ctx.Done()
    fmt.Printf("主流程结束: %v\n", ctx.Err())
}

输出

[任务A] 工作中...
[任务B] 工作中...
[任务A] 工作中...
[任务B] 工作中...
[任务A] 工作中...
[任务B] 工作中...
[任务A] 工作中...
[任务B] 工作中...
[任务A] 收到取消信号,退出: context deadline exceeded
[任务B] 收到取消信号,退出: context deadline exceeded
主流程结束: context deadline exceeded

核心模式select { case <-ctx.Done(): ... } 是检测 Context 取消的标准方式。


24.11 Context.Err():返回取消原因

Err() 方法返回 Context 被取消的原因。如果 Context 没有被取消,返回 nil

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
    // 测试主动取消
    ctx1, cancel1 := context.WithCancel(context.Background())
    cancel1()
    fmt.Printf("主动取消: %v\n", ctx1.Err())
    // 输出: 主动取消: context canceled

    // 测试超时取消
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Millisecond)
    defer cancel2()
    time.Sleep(10 * time.Millisecond)
    fmt.Printf("超时取消: %v\n", ctx2.Err())
    // 输出: 超时取消: context deadline exceeded

    // 未取消的 Context
    ctx3 := context.Background()
    fmt.Printf("未取消: %v\n", ctx3.Err())
    // 输出: 未取消: <nil>
}

两个标准的取消原因

常量含义触发场景
context.Canceled主动取消调用了 cancel() 函数
context.DeadlineExceeded超时取消超过了截止时间

24.12 Context.Deadline():返回截止时间

Deadline() 返回 Context 的截止时间。如果未设置截止时间,返回 (time.Time{}, false)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    // 无截止时间的 Context
    ctx1 := context.Background()
    deadline, ok := ctx1.Deadline()
    fmt.Printf("Background - 截止时间: %v, 已设置: %v\n", deadline, ok)
    // 输出: Background - 截止时间: 0001-01-01 00:00:00 +0000 UTC, 已设置: false

    // 有截止时间的 Context
    ctx2, _ := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
    deadline, ok = ctx2.Deadline()
    fmt.Printf("WithDeadline - 截止时间: %v, 已设置: %v\n", deadline.Format(time.RFC3339), ok)
    // 输出: WithDeadline - 截止时间: 2024-01-15T10:30:00+08:00, 已设置: true
}

使用场景:当你需要知道操作的最晚完成时间时,可以从 Context 中获取。比如定时任务调度、缓存过期判断等。


24.13 Context.Value:获取存储的值

Value(key) 方法用于从 Context 中获取值。它会沿着 Context 链从当前节点向父节点递归查找

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
    // 构建一个 Context 链
    level1 := context.WithValue(context.Background(), "level", 1)
    level2 := context.WithValue(level1, "level", 2)
    level3 := context.WithValue(level2, "level", 3)

    // 当前 Context 有值,直接返回
    fmt.Printf("level3.Value('level') = %v\n", level3.Value("level"))
    // 输出: 3

    // 当前 Context 没有,往上找
    fmt.Printf("level3.Value('name') = %v\n", level3.Value("name"))
    // 输出: <nil>

    // 从父节点查找
    fmt.Printf("level2.Value('level') = %v\n", level2.Value("level"))
    // 输出: 1
}

查找规则:先在当前 Context 查找,如果没有找到,继续在父 Context 中查找,递归向上,直到找到或者到达根节点(返回 nil)。


24.14 取消传播机制:Context 最重要的特性

Context 的取消机制具有树形传播特性:当父 Context 被取消时,所有派生的子 Context 都会被取消。这是 Context 最强大的特性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func worker(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("[%s] 被取消了: %v\n", name, ctx.Err())
            return
        default:
            fmt.Printf("[%s] 工作中...\n", name)
            time.Sleep(300 * time.Millisecond)
        }
    }
}

func main() {
    // 创建父 Context,超时 3 秒
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // 创建两个子 Context
    ctx1, _ := context.WithCancel(ctx)
    ctx2, _ := context.WithCancel(ctx)

    // 启动多个 worker
    go worker(ctx1, "Worker-A")
    go worker(ctx2, "Worker-B")
    go worker(ctx, "Worker-Main")

    // 等待 Context 取消
    <-ctx.Done()
    fmt.Println("父 Context 已超时,所有子 Context 都会被取消")
    time.Sleep(1 * time.Second)
}

取消传播的 Mermaid 图

graph TD
    A["Background"] --> B["WithTimeout (3s)"]
    B --> C["ctx1 (WithCancel)"]
    B --> D["ctx2 (WithCancel)"]
    B --> E["Worker-Main"]

    C --> F["Worker-A"]
    D --> G["Worker-B"]

    B -->|"超时取消"| H["所有子节点都收到取消信号"]
    H --> C
    H --> D
    H --> E

取消传播的威力:想象一个复杂的微服务调用链:

  1. 网关接收请求,创建一个带超时的 Context
  2. 调用 Service A,Service A 创建子 Context
  3. Service A 调用 Service B,Service B 创建子 Context
  4. 如果网关超时取消,所有正在进行的 Service 调用都会被自动取消

这就是 Context 的「取消信号沿着树向上传播」机制。


24.15 key 的设计规范:非导出类型作为 key

Context 的 Value()按指针相等(==)来比较 key 的。这意味着,如果两个不同的包都使用字符串 "requestID" 作为 key,它们实际上会冲突!

错误示例:不同包的 key 冲突

1
2
3
4
5
6
7
// package A
ctx := context.WithValue(ctx, "requestID", "123")

// package B(假设也用了 "requestID" 作为 key)
// 在 package A 的 Context 上调用 package B 的方法
// B 也想通过 "requestID" 存自己的值
ctx2 := context.WithValue(ctx, "requestID", "456") // 覆盖了 A 的值!

正确示例:使用非导出的类型作为 key

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 在你的包里定义一个非导出的 key 类型
type contextKey string

// 或者更推荐:使用 struct{} 作为 key,定义多个常量
type key int

const (
    requestIDKey key = iota
    userIDKey
    traceIDKey
)

// 使用时
ctx := context.WithValue(ctx, requestIDKey, "req-123")

完整的最佳实践示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package mypkg

// 定义非导出的 key 类型
type contextKey string

// 定义包级别的 key 常量
const (
    requestIDKey contextKey = "request-id"
    userIDKey    contextKey = "user-id"
)

// GetRequestID 安全地获取请求 ID
func GetRequestID(ctx context.Context) string {
    if v := ctx.Value(requestIDKey); v != nil {
        return v.(string)
    }
    return ""
}

// WithRequestID 存储请求 ID
func WithRequestID(ctx context.Context, requestID string) context.Context {
    return context.WithValue(ctx, requestIDKey, requestID)
}

核心原则:永远不要用字符串、int 等内建类型作为 Context 的 key,因为它们可能被不同的库覆盖。创建一个非导出的自定义类型作为 key,是避免冲突的最佳方式。


24.16 错误用法:用 WithValue 传递可选参数

WithValue 不是用来传递可选参数的! 这是一个常见的误解。

错误示例:把 Context 当成「可选参数包」

1
2
3
4
5
6
7
8
// 错误的做法
func fetchUser(ctx context.Context, id int) (*User, error) {
    // 错误:把 userID 当作可选参数存在 Context 里
    if userID := ctx.Value("currentUserID"); userID != nil {
        fmt.Printf("操作者: %v\n", userID)
    }
    return db.QueryUser(id)
}

为什么这是错的?

  1. 类型不安全:所有值都是 any,编译器无法检查类型
  2. 难以调试:值的来源不明确,容易产生困惑
  3. 语义混乱:Context 应该代表「请求的上下文」,而不是「参数容器」

正确做法:明确传递参数

1
2
3
4
5
// 正确的做法
func fetchUser(ctx context.Context, id int, operatorID string) (*User, error) {
    fmt.Printf("操作者: %s\n", operatorID)  // 明确传递,类型安全
    return db.QueryUser(ctx, id)
}

WithValue 的正确使用场景

WithValue 只应该用于传递请求级别的、多个函数都需要访问的、不应该作为显式参数的元数据

1
2
3
4
5
6
// 正确的使用场景
ctx := context.WithValue(ctx, requestIDKey, "req-123")
ctx = context.WithValue(ctx, traceIDKey, "trace-abc")

// 日志、监控、数据库等基础设施可以自动从 Context 读取
log.Info("处理请求", "requestID", ctx.Value(requestIDKey))

24.17 context 的使用规范

Go 官方博客和 Effective Go 中都有专门的 context 规范说明。以下是必须遵循的规则:

规范一:作为第一个参数传递

1
2
3
4
5
6
7
8
9
// ✅ 正确:Context 作为第一个参数
func FetchUser(ctx context.Context, userID int) (*User, error) {
    // ...
}

// ❌ 错误:Context 作为最后一个参数
func FetchUser(userID int, ctx context.Context) (*User, error) {
    // ...
}

规范二:只在必要时传递

1
2
3
4
5
6
7
8
9
func main() {
    ctx := context.Background()

    // ❌ 错误:每个函数都传 Context,即使它不需要
    result := process(ctx, data)

    // ✅ 正确:只在需要取消信号、截止时间或元数据时传递
    result = processWithTimeout(ctx, data)
}

规范三:不要用于可选参数

1
2
3
4
5
6
// ❌ 错误
func Query(ctx context.Context, id int, trace bool) { ... }

// ✅ 正确
func Query(ctx context.Context, id int) { ... }
func QueryWithTrace(ctx context.Context, id int, traceID string) { ... }

规范四:及时取消

1
2
3
4
5
6
func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()  // 防止资源泄漏

    // 使用 ctx 处理请求
}

24.18 标准库中的 Context

Go 标准库大量使用了 Context,以下是几个最常见的场景:

net/http.Request.Context()

HTTP 请求自带 Context,可以从中获取请求级别的数据和取消信号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // 从 Context 中提取数据
    requestID := ctx.Value("requestID")
    fmt.Printf("处理请求: %v\n", requestID)

    select {
    case <-time.After(5 * time.Second):
        w.Write([]byte("OK"))
    case <-ctx.Done():
        fmt.Printf("请求被取消: %v\n", ctx.Err())
    }
}

func main() {
    http.HandleFunc("/", handleRequest)
    http.ListenAndServe(":8080", nil)
}

database/sql.QueryContext

数据库操作支持 Context,可以设置超时和取消:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func queryWithTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    // QueryContext 支持 Context 参数
    rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", 1)
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("查询超时!")
        }
        return
    }
    defer rows.Close()

    // 处理结果
}

os/exec.CommandContext

执行外部命令时可以用 Context 控制超时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func runCommandWithTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // CommandContext 会在 Context 取消时杀死进程
    cmd := exec.CommandContext(ctx, "ping", "-n", "10", "localhost")
    output, err := cmd.CombinedOutput()

    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("命令执行超时!")
        }
        return
    }

    fmt.Printf("输出: %s\n", output)
}

24.19 context 的最佳实践:定期检查 Done()

最标准的 Context 使用模式是在长操作中定期检查 Done() 频道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func longRunningTask(ctx context.Context) error {
    for i := 0; i < 100; i++ {
        // 每一步都检查 Context 是否被取消
        select {
        case <-ctx.Done():
            return ctx.Err()  // 快速退出,释放资源
        default:
            // 继续执行
        }

        // 执行一些工作
        fmt.Printf("执行第 %d 步...\n", i)
        time.Sleep(200 * time.Millisecond)
    }
    return nil
}

完整示例:HTTP 服务中的 Context 使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

type contextKey string

const requestIDKey contextKey = "request-id"

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 生成请求 ID 并存入 Context
        requestID := fmt.Sprintf("req-%d", time.Now().UnixNano())
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)

        w.Header().Set("X-Request-ID", requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // 模拟处理逻辑,定期检查取消
    for i := 0; i < 5; i++ {
        select {
        case <-ctx.Done():
            fmt.Printf("请求被取消: %v\n", ctx.Err())
            return
        case <-time.After(1 * time.Second):
            fmt.Printf("[%s] 处理中...\n", ctx.Value(requestIDKey))
        }
    }

    w.Write([]byte("处理完成"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)

    server := &http.Server{
        Addr:    ":8080",
        Handler: middleware(mux),
    }

    fmt.Println("服务器启动在 :8080")
    server.ListenAndServe()
}

本章小结

本章我们深入探讨了 Go 标准库中的 context 包,它是 Go 并发编程中不可或缺的重要组成部分。

核心要点回顾

概念说明
Context 接口提供 Deadline()Done()Err()Value() 四个方法
Background vs TODOBackground 是真正的根 Context;TODO 是占位符
WithCancel创建可手动取消的 Context
WithDeadline创建带绝对截止时间的 Context
WithTimeout创建带相对超时的 Context,是 WithDeadline 的语法糖
WithCancelCauseGo 1.21+ 支持,可传递取消原因
WithValue存储请求级别的元数据
取消传播父 Context 取消,子 Context 全部取消
Key 设计使用非导出类型避免不同包的 key 冲突

黄金法则

  1. 把 Context 当作「取消信号 + 元数据容器」,而不是「可选参数包」
  2. 永远用非导出类型作为 WithValue 的 key
  3. 在长操作中定期检查 Done()
  4. Context 应该作为第一个参数传递
  5. 及时调用 cancel() 或使用 defer cancel() 避免资源泄漏

Context 使用场景速查

┌─────────────────────────────────────────────────────────────┐
│                    何时使用 Context                          │
├─────────────────────────────────────────────────────────────┤
│ ✅ Web 请求处理和超时控制                                    │
│ ✅ 数据库查询超时                                            │
│ ✅ 外部 API 调用超时                                         │
│ ✅ 微服务调用链的取消传播                                     │
│ ✅ 传递请求 ID、trace ID 等链路追踪数据                        │
│ ❌ 不要用来传递业务可选参数                                   │
│ ❌ 不要用来替换函数返回值                                     │
└─────────────────────────────────────────────────────────────┘

Context 是 Go 语言「简单、实用、可组合」哲学的完美体现。掌握它,你就能写出更健壮、更优雅的 Go 程序。记住:在 Go 的世界里,让取消信号流动起来,让资源及时释放,这是每个 Go 程序员的必修课

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)