跳到主要内容

Context

问题

Go 的 context.Context 是什么?为什么几乎所有库函数的第一个参数都是 ctx context.Context

答案

Context 的作用

context.Context 是 Go 并发编程的核心基础设施,用于:

  1. 取消信号传播——通知 goroutine 停止工作
  2. 超时/截止时间控制——避免 goroutine 永远等待
  3. 请求级数据传递——如 traceID、用户信息、请求元数据

四种派生函数

1. WithCancel

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 重要:必须调用,否则 ctx 泄漏

go func() {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信号:", ctx.Err()) // context canceled
return
default:
doWork()
}
}
}()

// 主动取消
cancel()

2. WithTimeout

// 3 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

select {
case result := <-doSlowWork(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("超时:", ctx.Err()) // context deadline exceeded
}

3. WithDeadline

// 设置截止时间为 2025-01-01 00:00:00
deadline := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

WithTimeout(parent, 3s) 本质等同于 WithDeadline(parent, time.Now().Add(3s))

4. WithValue

type contextKey string

const traceIDKey contextKey = "traceID"

// 存值
ctx := context.WithValue(context.Background(), traceIDKey, "abc-123")

// 取值
traceID := ctx.Value(traceIDKey).(string) // "abc-123"
WithValue 注意事项
  • key 必须是自定义非导出类型,避免冲突(不要用 stringint 做 key)
  • 只传请求级元数据(traceID、userID),不要传业务参数
  • Value 查找是链式向上遍历,性能 O(n)O(n),不要滥用

Context 传播规则

核心规则:父 Context 取消,所有子 Context 自动取消(向下传播)。子 Context 取消不影响父 Context。

实际使用模式

HTTP Server

func handler(w http.ResponseWriter, r *http.Request) {
// r.Context() 会在客户端断开连接时自动取消
ctx := r.Context()

result, err := queryDB(ctx, "SELECT ...")
if err != nil {
if ctx.Err() != nil {
// 客户端已断开,无需返回
return
}
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(result)
}

超时控制

func callExternalAPI(ctx context.Context) (*Response, error) {
// 为外部调用增加独立的超时
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
return http.DefaultClient.Do(req)
}

Go 1.21+: WithoutCancel / AfterFunc

// WithoutCancel:创建不随父 Context 取消的子 Context
// 适用于需要在请求结束后继续执行的异步任务(如日志记录)
ctx := context.WithoutCancel(parentCtx)

// AfterFunc:Context 取消时执行回调
stop := context.AfterFunc(ctx, func() {
fmt.Println("context 被取消了")
})
// stop() 可以阻止回调执行

常见面试问题

Q1: context.Background() 和 context.TODO() 的区别?

答案

两者本质相同,都是空 Context(永不取消、无超时、无值)。区别在于语义

  • Background():用于主函数、初始化、测试等顶层入口
  • TODO():用于不确定该传什么 Context 的地方,作为临时占位符

代码中出现 TODO() 意味着"这里以后需要替换掉"。

Q2: 为什么必须 defer cancel()?

答案

WithCancel / WithTimeout / WithDeadline 创建的 Context 会在内部启动 goroutine 或注册 timer 来监听取消。不调用 cancel() 会导致这些资源泄漏,直到父 Context 取消。

即使 Context 已经超时取消了,调用 cancel() 也是安全的(幂等),所以 defer cancel() 是固定写法。

Q3: 如何在 goroutine 中正确响应 Context 取消?

答案

func worker(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err() // 返回取消原因
default:
}

// 对于阻塞操作,用 select 同时监听
select {
case <-ctx.Done():
return ctx.Err()
case data := <-dataChan:
process(data)
}
}
}

关键点:每个可能阻塞的地方都要监听 ctx.Done()

Q4: Context 值传递有什么最佳实践?

答案

  1. key 必须是自定义非导出类型
  2. 只传请求级元数据(traceID、auth token),不传业务参数
  3. 提供类型安全的存取函数
type contextKey string
const userKey contextKey = "user"

func WithUser(ctx context.Context, user *User) context.Context {
return context.WithValue(ctx, userKey, user)
}

func UserFromContext(ctx context.Context) (*User, bool) {
user, ok := ctx.Value(userKey).(*User)
return user, ok
}

Q5: Context 的最佳实践有哪些?

答案

  1. Context 作为函数第一个参数func DoSomething(ctx context.Context, arg string)
  2. 不要把 Context 存在 struct 中(除非有充分理由)
  3. 不要传 nil Context:不确定时用 context.TODO()
  4. 每次派生都 defer cancel()
  5. 不要用 Context 传业务参数:只传请求级元数据
  6. 合理设置超时:外部调用必须有超时

相关链接