context — 上下文传递与取消
context 是 Go 并发编程中使用最广、误解最深的包。它在 goroutine 之间传递:截止时间、取消信号和请求范围的值。
1. 四种创建方式
// 根 context(通常在整个程序入口使用)
ctx := context.Background() // 用于 main、init、测试
ctx := context.TODO() // 占位符:表示"还没想好用哪种 "
// 派生的 context
ctx, cancel := context.WithCancel(parent) // 手动取消
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // 绝对超时
ctx, cancel := context.WithDeadline(parent, deadline) // 截止时间
ctx := context.WithValue(parent, key, value) // 携带值🚨 顶级陷阱:
WithCancel/WithTimeout/WithDeadline返回的cancel函数必须被调用,否则会 goroutine 泄漏。Go 官方明确说:“即使 ctx 已经超时自动取消,调用 cancel 仍然是安全且推荐的。“始终用defer cancel()。
2. 取消传播机制
2.1 Parent-Child 取消链
parentCtx, cancel := context.WithCancel(context.Background())
defer cancel()
childCtx, _ := context.WithCancel(parentCtx)
// 取消 parent → child 也被取消
// 取消 child → parent 不受影响
cancel() // parent 和 child 都收到 Done() signal2.2 🔬 深入原理:ctx.Done() 的工作原理
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done(): // ← 返回一个只读 channel
fmt.Println("cancelled:", ctx.Err())
return
default:
doWork()
}
}
}ctx.Done()返回的 channel 永不发送数据,只是被关闭。- 关闭的 channel 立即可读(零值),所以所有阻塞在
<-ctx.Done()上的 goroutine 同时被唤醒。 ctx.Err()返回取消原因:context.Canceled(手动取消)或context.DeadlineExceeded(超时)。
2.3 正确使用 select
// ✅ 正确 — 检查 ctx.Done() 做分支
select {
case <-ctx.Done():
return ctx.Err()
case result := <-workCh:
return process(result)
}
// ❌ 错误 — 只是在 select 中使用 time.After
select {
case <-time.After(timeout):
return ErrTimeout
case result := <-workCh:
return process(result)
}
// 问题:不会响应上层传递的取消信号。如果 ctx 被取消,这个 goroutine 仍然阻塞。3. Context 传递值
3.1 规范做法
// 1. 定义私有 key 类型(避免冲突)
type contextKey string // 或用 struct{} 更安全
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
// 2. 封装存取函数
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}🚨 陷阱:
- 不要用
string直接当 key。不同包可能使用相同的 key 字符串,造成覆盖。使用自定义类型(type myKey struct{})作为 key。- 不要用 Context 传业务参数。Context 只适合传请求范围的元数据(request ID、trace ID、认证信息),不适合传函数参数。
3.2 Context 查找链
// WithValue 在父 ctx 基础上添加键值对(不可变设计)
ctx := context.Background()
ctx = context.WithValue(ctx, key1, "val1")
ctx = context.WithValue(ctx, key2, "val2")
// 查找链:ctx → key2(不匹配)→ 父 ctx → key1(匹配)
// 时间复杂度:O(depth),不建议嵌套太深4. 超时控制的经典模式
4.1 HTTP 请求超时
func fetchURL(ctx context.Context, url string) ([]byte, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// 调用方设置总体超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
data, err := fetchURL(ctx, "https://example.com")4.2 数据库查询超时
func queryUsers(ctx context.Context, db *sql.DB, name string) ([]User, error) {
// 为单个查询再设置更短的超时
queryCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
return db.QueryContext(queryCtx, "SELECT * FROM users WHERE name = ?", name)
}4.3 串行操作 vs 并行操作
// 串行:每个操作独立超时
func serialWork(ctx context.Context) error {
// 操作 A: 最多 1s
ctxA, cancelA := context.WithTimeout(ctx, time.Second)
defer cancelA()
if err := doA(ctxA); err != nil {
return err
}
// 操作 B: 最多 2s
ctxB, cancelB := context.WithTimeout(ctx, 2*time.Second)
defer cancelB()
return doB(ctxB)
}
// 并行:同时启动,等任意完成或全部超时
func parallelWork(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := make(chan error, 2)
go func() { ch <- doA(ctx) }()
go func() { ch <- doB(ctx) }()
for i := 0; i < 2; i++ {
if err := <-ch; err != nil {
return err // ← 隐式触发 cancel(),另一个 goroutine 被取消
}
}
return nil
}5. 🚨 Context 使用规范(官方推荐)
- 不要存储在结构体中。Context 应该作为函数第一个参数传递(通常是
ctx)。 - Context 参数总在第一位。
func DoSomething(ctx context.Context, arg string) error - 不要传 nil。如果不确定用什么,用
context.TODO()。 - 不要将 Context 用于传递可选参数。用 struct 或 options 模式。
ctx.Value仅用于请求范围的跨 API 边界数据。- 始终调用 cancel 函数。即使 ctx 已自动取消。
- Context 是并发安全的。可以在多个 goroutine 中同时使用。
6. 高级:自定义 Context 包装
// 为 Context 添加默认超时的包装器
type timeoutContext struct {
context.Context
}
func (c *timeoutContext) Deadline() (time.Time, bool) {
d, ok := c.Context.Deadline()
if !ok {
return time.Now().Add(30 * time.Second), true
}
return d, true
}
func WithDefaultTimeout(parent context.Context) context.Context {
return &timeoutContext{parent}
}