2025-12-26 · 实战
32
实战 · 2025-12-26

Go 语言 Context:并发控制的标准答案

Go 语言 Context:并发控制的标准答案

Go 语言的 context 包是并发编程的"任务指挥官"。它管理 goroutine 的生命周期、传播取消信号、传递截止时间。


一、为什么需要 Context?

没有 Context 的痛苦

问题
后果

Goroutine 失控
资源泄漏、后台幽灵任务

停止信号难传递
需要自己设计 channel 机制

请求数据传递混乱
函数签名冗长、耦合度高

Context 的核心价值

生活比喻:Context 就像派对策划人
- 知道派对何时开始、何时结束(Deadline/Timeout)
- 能提前通知所有人"派对取消"(Cancellation)
- 携带特殊要求(如着装主题)(Value)

技术价值
- 统一的生命周期管理
- 标准化的取消信号传播
- 优雅的超时控制
- 请求范围数据传递


二、Context 接口:四个核心方法

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

方法
作用

Deadline()
查询是否设置了截止时间

Done()
返回只读 channel,关闭时表示取消

Err()
获取取消原因(Canceled/DeadlineExceeded)

Value()
检索键值对数据

关键理解:Context 是一种"协作契约"。函数接受 Context 意味着承诺"倾听并遵守"取消信号。


三、创建根 Context

context.Background()

特性:永不取消、无值、无截止时间

使用场景
- main() 函数启动服务
- 单元测试
- 服务器处理入站请求的顶层

context.TODO()

特性:功能同 Background,但语义是"临时方案"

使用场景
- 不确定该用哪个 Context 时
- 计划后续重构时

区别:Background 是"正式起点",TODO 是"待办标记"

// 正确
rootCtx := context.Background()

// 错误!永远不要传递 nil
// var ctx context.Context = nil

四、派生子 Context

4.1 可取消:WithCancel

ctx, cancel := context.WithCancel(parentCtx)
defer cancel()  // 必须调用,释放资源

工作原理
- 调用 cancel() 函数关闭 ctx.Done() channel
- 取消信号自动传播到所有子 Context
- 必须配合 defer cancel() 使用

4.2 超时控制:WithTimeout

timeoutCtx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

特点:相对时间,2 秒后自动取消

本质WithTimeoutWithDeadline 的便捷包装

4.3 截止时间:WithDeadline

deadlineCtx, cancel := context.WithDeadline(parentCtx, time.Date(2025, 12, 31, 23, 59, 59, time.UTC))
defer cancel()

特点:绝对时间点,到达即取消

4.4 传递数据:WithValue

type userIDKey string

ctx := context.WithValue(parentCtx, userIDKey("userID"), 123)
userID := ctx.Value(userIDKey).(int)

最佳实践
- 定义自定义键类型(避免冲突)
- 只传递请求范围数据
- 不要传递普通函数参数


五、完整示例

package main

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

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-time.After(1 * time.Second):
            fmt.Printf("Worker %d: working...\n", id)
        case <-ctx.Done():
            fmt.Printf("Worker %d: stopped, reason: %v\n", id, ctx.Err())
            return
        }
    }
}

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

    // 3 秒超时
    timeoutCtx, cancel := context.WithTimeout(rootCtx, 3*time.Second)
    defer cancel()

    // 启动两个 worker
    go worker(timeoutCtx, 1)
    go worker(timeoutCtx, 2)

    time.Sleep(5 * time.Second)
    fmt.Println("Main: exiting")
}

输出

Worker 1: working...
Worker 2: working...
Worker 1: working...
Worker 2: working...
Worker 1: stopped, reason: context deadline exceeded
Worker 2: stopped, reason: context deadline exceeded
Main: exiting

六、常见陷阱

陷阱
后果
解决

忘记调用 cancel()
资源泄漏
始终使用 defer cancel()

传递 nil Context
可能 panic
context.Background()

Value 传递函数参数
代码不清晰
用正常的函数参数

子 Context 不检查 Done()
无法响应取消
在循环中 select 监听


七、小结

Context 的本质
- ✅ 并发控制的标准方案
- ✅ 生命周期管理的优雅工具
- ✅ Go 协作式并发模型的核心

核心原则
1. 永远不要传递 nil Context
2. 始终调用 defer cancel()
3. 在 goroutine 中检查 ctx.Done()
4. Value 只传请求范围数据

何时使用
- HTTP 请求处理
- 数据库/网络调用
- 任何需要控制生命周期的操作


参考链接
- Go 官方博客:https://go.dev/blog/context
- Context 包文档:https://pkg.go.dev/context
- 原文来源:docs.80aj.com

目录 最新
← 左侧翻上一屏 · 右侧翻下一屏 · 中间唤出菜单