Go context 包详解
一句话总结
context 是 Go 并发编程的核心工具,用于在 goroutine 之间传递截止时间、取消信号和请求级元数据。
为什么需要 context?
HTTP 请求 → 启动多个 goroutine(查DB、调RPC、查缓存)
↓
请求被客户端取消 → 所有子 goroutine 必须立即停止,释放资源
没有 context,你只能手动传 done chan struct{},代码会变得很丑且容易泄漏 goroutine。
四种创建方式
1. Background() --- 根上下文
go
// 最顶层使用,永不取消,永不超时
ctx := context.Background()
2. TODO() --- 占位用
go
// 还不确定用啥时先用 TODO,后续替换
ctx := context.TODO()
3. WithCancel --- 手动取消
go
ctx, cancel := context.WithCancel(parentCtx)
// 某个条件触发时调用
cancel() // 向所有监听 ctx.Done() 的 goroutine 发送取消信号
典型场景:用户主动取消请求、前置条件不满足时中断下游。
4. WithTimeout --- 超时取消
go
// 3秒后自动取消(内部也是 WithCancel + timer)
ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second)
defer cancel() // 养成好习惯,避免泄漏
5. WithDeadline --- 绝对时间取消
go
// 到指定时间点自动取消
ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Minute))
defer cancel()
6. WithValue --- 传递请求级元数据
go
type contextKey string
const traceIDKey contextKey = "traceID"
ctx := context.WithValue(parentCtx, traceIDKey, "abc-123")
val := ctx.Value(traceIDKey) // "abc-123"
核心 API
| 方法 | 返回值 | 作用 |
|---|---|---|
ctx.Done() |
<-chan struct{} |
取消时关闭,用于 select 监听 |
ctx.Err() |
error |
返回 Canceled 或 DeadlineExceeded |
ctx.Value(key) |
any |
读取上下文值 |
ctx.Deadline() |
(time.Time, bool) |
返回截止时间 |
实战示例
示例 1:超时控制
go
func fetchWithTimeout(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err // 超时会返回 context.DeadlineExceeded
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
示例 2:链式取消(扇出)
go
func processOrder(ctx context.Context, orderID string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
errCh := make(chan error, 3)
// 并行调用三个服务
services := []string{"inventory", "payment", "notification"}
for _, svc := range services {
wg.Add(1)
go func(name string) {
defer wg.Done()
if err := callService(ctx, name, orderID); err != nil {
errCh <- fmt.Errorf("%s failed: %w", name, err)
cancel() // 一个失败,全部取消
}
}(svc)
}
go func() {
wg.Wait()
close(errCh)
}()
for err := range errCh {
return err // 返回第一个错误
}
return nil
}
示例 3:goroutine 正确退出模式
go
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case <-ctx.Done():
// 收到取消信号,清理并退出
fmt.Println("worker exiting:", ctx.Err())
return
case val, ok := <-ch:
if !ok {
return // channel 关闭
}
process(val)
}
}
}
上下文传播链(重要!)
Background
└─ WithTimeout(5s) → ctx1
└─ WithCancel() → ctx2
└─ WithValue() → ctx3
关键规则:
- 子 context 取消 → 父 context 不受影响
- 父 context 取消 → 所有子 context 级联取消 (这就是
WithCancel传 parent 的原因) - 形成树状结构,非常适合 HTTP 请求的生命周期管理
最佳实践
✅ DO
go
// 1. 函数第一个参数放 context,命名为 ctx
func DoSomething(ctx context.Context, arg string) error { ... }
// 2. 不要把 context 存在结构体里,通过参数传递
// 3. 用自定义类型做 key,避免冲突
type myKey string
ctx := context.WithValue(parent, myKey("userID"), 42)
// 4. defer cancel(),防止泄漏
ctx, cancel := context.WithTimeout(parent, timeout)
defer cancel()
❌ DON'T
go
// 1. 不要传 nil context
DoSomething(nil, "bad") // 应该用 context.Background()
// 2. 不要用 context.Value 传业务参数
ctx := context.WithValue(parent, "userID", 42) // string key 容易冲突
// 3. 不要把 context 放进结构体
type Service struct {
ctx context.Context // ❌ 应通过方法参数传递
}
context 的性能考量
| 操作 | 耗时 | 说明 |
|---|---|---|
WithCancel |
~30ns | 极轻量,只是创建结构体 |
WithValue |
~50ns | 轻量,用链表存储 |
ctx.Done() |
~5ns | 只是读 channel |
ctx.Value() |
O(n) | 沿链表查找,n = value 层数 |
结论 :正常使用基本不需要担心性能。唯一要注意的是 Value() 不要嵌套太深。
一张图总结
context.Background()
│
┌────────┼────────┐
WithTimeout WithCancel WithValue
│ │ │
┌────┘ ┌────┘ ┌────┘
子goroutine 子goroutine 子goroutine
cancel() 或 超时到期 或 Deadline 到达
↓
ctx.Done() 关闭 → 所有 select 监听收到信号 → goroutine 退出
核心思想就一句话:context 是 goroutine 的生命控制器。