Go Context 超时控制与并发取消最佳实践
目录
前言
在 Go 后端开发中,context.Context 无处不在。从 HTTP 请求处理到数据库查询、RPC 调用,Context 承载着超时控制、取消信号、元数据传递等重要职责。很多初学者只把它当作"传参工具",而忽略了它真正的设计精髓。
Context 的核心能力
Context 本质上是一个并发安全的树状结构,提供三个核心能力:
- 取消传播:父 Context 取消时,所有子 Context 自动取消
- 超时控制:到期自动触发取消
- 值传递:在请求链路中传递请求级别的元数据
超时控制实战
基础用法:WithTimeout
func QueryDatabase(ctx context.Context) ([]User, error) {
// 设置 3 秒超时
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // 重要!防止资源泄漏
// 模拟数据库查询
ch := make(chan result)
go func() {
users, err := actualDBQuery(ctx)
ch <- result{users, err}
}()
select {
case <-ctx.Done():
return nil, ctx.Err() // DeadlineExceeded
case r := <-ch:
return r.users, r.err
}
}分层超时:API → Service → DB
// HTTP Handler 层:5 秒超时
func UserHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
users, err := userService.GetUsers(ctx, params)
// ...
}
// Service 层:内部再设 3 秒
func (s *UserService) GetUsers(ctx context.Context, p Params) ([]User, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
return s.db.FindUsers(ctx, p)
}
// DB 层:数据库查询支持 Context
func (db *DB) FindUsers(ctx context.Context, p Params) ([]User, error) {
rows, err := db.pool.QueryContext(ctx, query, p.ID)
// ...
}最佳实践:每层调用时,使用父 Context 派生更短超时的子 Context,实现"上游比下游更宽松"的超时策略。
并发取消传播
扇出模式(Fan-Out)的统一取消
func BatchProcess(ctx context.Context, items []Item) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errCh := make(chan error, len(items))
for _, item := range items {
go func(item Item) {
select {
case errCh <- processItem(ctx, item):
case <-ctx.Done():
}
}(item)
}
// 任何一个失败,就取消所有
for range items {
if err := <-errCh; err != nil {
return err // 触发 defer cancel()
}
}
return nil
}优雅关闭服务
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 监听退出信号
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
cancel() // 收到信号,取消所有后台任务
}()
// 启动多个服务
go startHTTPServer(ctx)
go startGRPCServer(ctx)
go startConsumer(ctx)
<-ctx.Done()
// 执行优雅关闭...
time.Sleep(5 * time.Second)
os.Exit(0)
}WithValue 的正确用法
context.WithValue 只应该传递请求 scope 的数据,不要替代函数参数:
// ✅ 正确:请求链路标识
ctx = context.WithValue(ctx, TraceKey, traceID)
ctx = context.WithValue(ctx, UserKey, currentUser)
// ❌ 错误:业务参数应该显式传递
ctx = context.WithValue(ctx, "db_timeout", "5s") // Bad!
ctx = context.WithValue(ctx, "page_size", 20) // Bad!常见陷阱
陷阱一:忘记调用 cancel
// ❌ 资源泄漏:cancel 未调用
func BadTimeout(ctx context.Context) {
ctx, _ = context.WithTimeout(ctx, time.Second)
// cancel 没有被调用,定时器会一直运行
}
// ✅ 正确:永远 defer cancel()
func GoodTimeout(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
}陷阱二:在 goroutine 中持有父 Context
// ❌ 危险:闭包持有父 ctx,可能导致 goroutine 泄漏
func BadLaunch() {
ctx := context.Background()
for i := 0; i < 10; i++ {
go func() {
doWork(ctx) // 父 ctx 一直存在
}()
}
}
// ✅ 正确:为每个 goroutine 派生独立的子 Context
func GoodLaunch() {
parent := context.Background()
for i := 0; i < 10; i++ {
ctx, cancel := context.WithCancel(parent)
go func() {
defer cancel()
doWork(ctx)
}()
}
}总结
Context 是 Go 并发编程的基础设施,用好的关键是记住三个原则:
- Always defer cancel() — 避免定时器泄漏
- 分层超时 — 每层设置自己的超时,不要依赖上游
- Context 不是万能箱 — 只放请求链路元数据,不放业务参数
掌握 Context,你的 Go 后端代码在健壮性和可维护性上会上一个大台阶。