Go 高手(02):context 包——超时取消、传值、链路追踪
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 2 篇,接 内存模型。Go 的context包是并发编程的核心基础设施——传递超时、取消信号、请求作用域的值。HTTP 服务、数据库操作、RPC 调用都依赖它。
本文要回答的问题
context.Background()和context.TODO()有什么区别?WithCancel、WithTimeout、WithDeadline怎么用?什么时候用哪个?ctx.Done()返回的 channel 什么时候收到值?WithValue传值有什么限制?为什么不用map[string]any替代?- context 链怎么传递?父 cancel 子会怎样?子 cancel 父会怎样?
一、context 的基本概念
context 的核心接口:
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回截止时间
Done() <-chan struct{} // 返回一个 channel,取消时收到值
Err() error // 返回取消原因
Value(key any) any // 返回绑定的值
}context 的本质是一条链上的"信号传递"机制——父 context 取消,所有子 context 也取消。但子 context 取消不会影响父 context。
二、创建 context 的入口
ctx := context.Background() // 根节点,通常用在 main 函数、初始化、测试里
ctx := context.TODO() // 还不确定用什么 context,先占位Background() 是根,永远不会取消。TODO() 在语义上也是根,但表示"代码还没想好用什么 context",方便以后替换。
三、WithCancel:手动取消
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 干活
select {
case <-ctx.Done():
return // 被取消了,退出
default:
// 继续干
}
}()
// 某个时刻,取消
cancel()WithCancel 返回一个 cancel 函数,调用 cancel() 会关闭 ctx.Done() 返回的 channel,所有监听这个 channel 的 goroutine 都会收到信号。
必须调用 cancel 释放资源:WithCancel 创建的 goroutine 持有内部资源,如果不调用 cancel(),会泄漏。
// 正确模式:defer cancel
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保 cancel 被调用四、WithTimeout 和 WithDeadline:超时控制
// 超时:3 秒后自动取消
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 截止时间:指定时间点取消
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()WithTimeout 内部就是调用了 WithDeadline。两个都返回 cancel 函数,提前完成时调 cancel 释放资源。
// 完整示例:HTTP 请求超时
func fetch(url string, timeout time.Duration) (*http.Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
return http.DefaultClient.Do(req)
// 超时了,ctx.Done() 收到信号,http 请求自动取消
}五、WithValue:传递请求作用域的值
type traceIDKey struct{}
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, traceIDKey{}, traceID)
}
func GetTraceID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(traceIDKey{}).(string)
return id, ok
}
// 使用
ctx = WithTraceID(ctx, "abc-123")
// 在另一个函数里
id, _ := GetTraceID(ctx)
fmt.Println(id) // abc-123WithValue 的规则:
- key 必须是自定义类型,不要用 string 做 key,避免不同包冲突
- value 必须是线程安全的(不可变的)
- 只传请求作用域的数据——trace ID、认证信息,不要传可选参数
- 取值是 O(n) 的,要在 context 链上逐层查找
六、context 链的传递规则
Background()
└── WithCancel (父)
├── WithTimeout (子 1)
│ └── WithValue (孙)
└── WithCancel (子 2)传递规则:
- 父 cancel → 所有子、孙都 cancel(
Done()收到信号) - 子 cancel → 不影响父和其他兄弟
- WithValue 不影响 cancel 链
parentCtx, parentCancel := context.WithCancel(context.Background())
childCtx, _ := context.WithTimeout(parentCtx, 5*time.Second)
parentCancel() // 父取消,子也立即取消
// childCtx.Done() 立刻收到信号,不管 5 秒 timeout 还剩多久七、context 在 HTTP 服务中的标准用法
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 从请求拿 context
result, err := queryDatabase(ctx, "SELECT ...")
if err != nil {
log.Printf("query failed: %v", err)
http.Error(w, "Internal Server Error", 500)
return
}
json.NewEncoder(w).Encode(result)
}
func queryDatabase(ctx context.Context, query string) ([]Result, error) {
// 检查是否已取消
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// 执行查询...
}客户端断开连接时,r.Context() 的 Done() 会自动收到信号,goroutine 可以及时退出,避免资源浪费。
八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 忘记调用 cancel | context 持有的 goroutine 泄漏 | 立即 defer cancel() |
| 用 string 做 WithValue 的 key | 不同包 key 冲突 | 用自定义空结构体类型 |
| 把 context 存在结构体里 | 设计错误,context 应该显式传递 | 函数第一个参数传 ctx |
| WithValue 传可选参数 | 参数传递不透明,很难维护 | 用显式参数 |
| 不必要的 context 包装 | 链太长,查找慢 | 只在需要时才包装 |
相关与延伸
下一篇:io 包与接口设计——Reader/Writer 组合接口;context 的底层实现是基于 channel 的 cancel 广播,见 goroutine 与 channel 深度解析;HTTP 服务中 context 的传递,见 net/http 深入。
一句话总结
Go context 包:Background() 是根,WithCancel 手动取消,WithTimeout 超时自动取消,WithValue 传请求作用域的值;父 cancel 所有子都 cancel,子 cancel 不影响父;defer cancel() 确保资源释放;context 要显式传递,不要存在结构体里;key 用自定义类型,不要用 string。