5. context 实战应用
5. context 实战应用
前面四篇讲了机制和模式。这一篇落到两个实际场景:怎么在测试里用 context,以及一个完整请求链路的综合示例,把所有知识点串起来。
测试中的五种 context 模式
1. Background 作为默认 context
绝大多数单元测试不需要复杂的 context,直接用 Background():
func TestProcess(t *testing.T) {
result := process(context.Background())
if result != "ok" {
t.Errorf("expected ok, got %s", result)
}
}
被测函数接收 context.Context 参数,测试时就传 Background(),简单清晰。
2. WithValue 注入 mock 数据
被测函数依赖 ctx 里的某些值(比如认证信息),测试时用 WithValue 注入:
type authInfo struct {
UserID int
Role string
}
func getUserRole(ctx context.Context) string {
auth, ok := ctx.Value("auth").(*authInfo)
if !ok {
return "anonymous"
}
return auth.Role
}
func TestGetUserRole(t *testing.T) {
// 注入管理员角色
ctx := context.WithValue(context.Background(), "auth", &authInfo{
UserID: 1,
Role: "admin",
})
if role := getUserRole(ctx); role != "admin" {
t.Errorf("expected admin, got %s", role)
}
// 测试无认证场景
if role := getUserRole(context.Background()); role != "anonymous" {
t.Errorf("expected anonymous, got %s", role)
}
}
3. WithCancel 模拟取消
测试”被取消后函数返回什么错误”:
func TestLongProcessCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(50 * time.Millisecond)
cancel() // 主动触发取消
}()
result, err := longProcess(ctx)
if err != context.Canceled {
t.Errorf("expected Canceled, got %v", err)
}
}
4. WithTimeout 防止测试卡住
测试里调用一个可能永远阻塞的函数,用 WithTimeout 兜底,防止 CI 永远挂住:
func TestProcessWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := process(ctx); err != nil {
t.Fatalf("process failed: %v", err)
}
}
5 秒后自动失败,比单元测试卡死 30 分钟强得多。
5. t.Context()(Go 1.24+)
Go 1.24 引入了 t.Context(),自动随测试结束取消:
func TestWorker(t *testing.T) {
ctx := t.Context() // 测试结束自动取消
if err := worker(ctx); err != nil {
t.Fatal(err)
}
}
省掉手动 cancel() 的样板代码。新项目用 Go 1.24+ 的话,这是首选。
综合示例:完整请求处理链路
把前四篇的所有知识点串成一个真实场景:一次 HTTP 请求从入口到响应,经历中间件、认证、超时控制、并发调用、清理回调。
链路结构
1. 入口 → 创建基础 context
2. Tracing 中间件 → WithValue 注入 trace_id
3. Auth 中间件 → WithValue 注入 user_id
4. Handler → WithTimeout 给整个请求设超时
5. Service → 继承 context
6. 并发调用 → Fan-out 调三个服务,WaitGroup 收集
7. 响应 → 返回聚合结果
8. AfterFunc → context 取消时记录日志
完整代码
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Request struct {
Path string
ClientIP string
}
type Response struct {
Data string
TraceID string
UserID string
Duration time.Duration
}
type Middleware func(ctx context.Context) context.Context
func handleRequest(req *Request) *Response {
start := time.Now()
// 1. 基础 context
baseCtx := context.Background()
// 2. 中间件:注入 TraceID
ctx := tracingMiddleware(baseCtx)
// 3. 中间件:认证,注入 UserID
ctx = authMiddleware(ctx)
// 4. 设置请求超时
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
// 5. 注册取消回调(用于日志记录)
context.AfterFunc(ctx, func() {
traceID, _ := ctx.Value("trace_id").(string)
fmt.Printf("[AfterFunc] context 取消, traceID=%s\n", traceID)
})
// 6. 并发调用多个服务
result, err := fanOutCall(ctx)
if err != nil {
return &Response{
Data: fmt.Sprintf("错误: %v", err),
TraceID: ctx.Value("trace_id").(string),
Duration: time.Since(start),
}
}
return &Response{
Data: result,
TraceID: ctx.Value("trace_id").(string),
UserID: ctx.Value("user_id").(string),
Duration: time.Since(start),
}
}
func tracingMiddleware(ctx context.Context) context.Context {
traceID := fmt.Sprintf("trace-%d", time.Now().UnixNano()%10000)
return context.WithValue(ctx, "trace_id", traceID)
}
func authMiddleware(ctx context.Context) context.Context {
return context.WithValue(ctx, "user_id", "user-42")
}
func fanOutCall(ctx context.Context) (string, error) {
services := []struct {
name string
delay time.Duration
}{
{"UserService", 100 * time.Millisecond},
{"OrderService", 200 * time.Millisecond},
{"PaymentService", 300 * time.Millisecond},
}
var wg sync.WaitGroup
results := make(chan string, len(services))
errors := make(chan error, len(services))
for _, svc := range services {
wg.Add(1)
go func(name string, delay time.Duration) {
defer wg.Done()
if r, err := callService(ctx, name, delay); err != nil {
errors <- err
} else {
results <- r
}
}(svc.name, svc.delay)
}
go func() {
wg.Wait()
close(results)
close(errors)
}()
var collected []string
for r := range results {
collected = append(collected, r)
}
select {
case err := <-errors:
return "", err
default:
return fmt.Sprintf("聚合 %d 个服务: %v", len(collected), collected), nil
}
}
func callService(ctx context.Context, name string, delay time.Duration) (string, error) {
traceID, _ := ctx.Value("trace_id").(string)
select {
case <-time.After(delay):
return fmt.Sprintf("%s OK (trace=%s)", name, traceID), nil
case <-ctx.Done():
return "", fmt.Errorf("%s 超时: %w", name, ctx.Err())
}
}
用到的知识点
这段代码把前面四篇的核心知识点全部串起来:
| 知识点 | 在代码里的体现 |
|---|---|
Background 作为根 | baseCtx := context.Background() |
WithValue 链式传递 | 中间件依次注入 trace_id、user_id |
WithTimeout | ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) |
defer cancel() | 创建后立即 defer |
AfterFunc | 注册取消后的日志回调 |
| Fan-out 并发 | 三个 goroutine 同时调三个服务 |
| WaitGroup + 关闭 channel | 收集所有结果 |
select + ctx.Done() | callService 里二选一 |
errors.Is 兼容的错误处理 | 用 %w 包装错误 |
| Value 向上查找 | 子 context 能拿到祖先所有值 |
模拟超时场景
把整体超时从 500ms 改成 250ms,会看到 PaymentService(300ms)超时:
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
// UserService(100ms) → 成功
// OrderService(200ms) → 成功
// PaymentService(300ms) → 失败: PaymentService 超时: context deadline exceeded
这正是 context 的核心价值:用一行 WithTimeout,让整个调用链都有超时保护,单个服务慢不会拖垮整个请求。
知识体系总览
整个 context 包的体系可以用一棵树概括:
context 包
├── 创建函数
│ ├── Background() 空 context,用作根
│ ├── TODO() 占位 context
│ ├── WithCancel(parent) 可手动取消
│ ├── WithTimeout(parent, d) 超时自动取消
│ ├── WithDeadline(parent, t) 截止时间取消
│ └── WithValue(parent, k, v) 携带键值对
│
├── Context 接口
│ ├── Deadline() 返回截止时间
│ ├── Done() 返回取消信号 channel
│ ├── Err() 返回取消原因
│ └── Value(key) 返回 key 对应的值
│
├── 传播规则
│ ├── 取消:父 → 子(单向)
│ ├── 值: 子 → 父(向上查找)
│ └── Deadline:取最早的
│
├── 错误类型
│ ├── Canceled 主动取消
│ └── DeadlineExceeded 超时取消
│
└── Go 1.21+ 新特性
├── AfterFunc(ctx, f) 取消后执行回调
└── WithoutCancel(parent) 不受父取消影响
最重要的两条原则,再强调一次:
select + ctx.Done()是 context 的灵魂。任何 goroutine,只要它的生命周期长于一次函数调用,都要监听ctx.Done()defer cancel()是底线。任何创建可取消 context 的地方,立即 defer cancel,不依赖”我知道这里要取消”