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 秒后自动取消
本质:WithTimeout 是 WithDeadline 的便捷包装
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