Context
问题
Go 的 context.Context 是什么?为什么几乎所有库函数的第一个参数都是 ctx context.Context?
答案
Context 的作用
context.Context 是 Go 并发编程的核心基础设施,用于:
- 取消信号传播——通知 goroutine 停止工作
- 超时/截止时间控制——避免 goroutine 永远等待
- 请求级数据传递——如 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 必须是自定义非导出类型,避免冲突(不要用
string或int做 key) - 只传请求级元数据(traceID、userID),不要传业务参数
- Value 查找是链式向上遍历,性能 ,不要滥用
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 值传递有什么最佳实践?
答案:
- key 必须是自定义非导出类型
- 只传请求级元数据(traceID、auth token),不传业务参数
- 提供类型安全的存取函数:
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 的最佳实践有哪些?
答案:
- Context 作为函数第一个参数:
func DoSomething(ctx context.Context, arg string) - 不要把 Context 存在 struct 中(除非有充分理由)
- 不要传 nil Context:不确定时用
context.TODO() - 每次派生都 defer cancel()
- 不要用 Context 传业务参数:只传请求级元数据
- 合理设置超时:外部调用必须有超时