Go context详解:超时控制与请求链路追踪

刚写Go那会,context对我来说就是个"到处传的参数",函数签名里写上但也不知道有什么用。

后来线上出了几次问题才明白:context是Go并发控制的灵魂


context解决什么问题

想象一个场景:用户请求进来,你要调用3个下游服务,汇总结果返回。

go 复制代码
func handleRequest(w http.ResponseWriter, r *http.Request) {
    result1 := callServiceA()
    result2 := callServiceB()
    result3 := callServiceC()
    
    response := merge(result1, result2, result3)
    json.NewEncoder(w).Encode(response)
}

问题来了:

  1. 如果用户取消请求(关闭浏览器),下游调用还在跑,浪费资源
  2. 如果ServiceA超时了,ServiceB和C还要继续等吗?
  3. 请求的一些上下文信息(用户ID、TraceID)怎么传下去?

context就是解决这些问题的。


context的四种创建方式

1. context.Background()

go 复制代码
ctx := context.Background()

空context,通常作为根context,在main函数或请求入口使用。

2. context.TODO()

go 复制代码
ctx := context.TODO()

也是空context,用于"还不确定用什么context"的场景。语义上表示待定。

3. context.WithCancel

go 复制代码
ctx, cancel := context.WithCancel(context.Background())
defer cancel()  // 一定要调用,否则会泄漏

go func() {
    // 做一些事情
    select {
    case <-ctx.Done():
        fmt.Println("被取消了")
        return
    case <-time.After(10 * time.Second):
        fmt.Println("完成")
    }
}()

// 某个条件触发取消
cancel()

关键点

  • cancel()必须调用,通常defer cancel()
  • 取消是传播的,父context取消,所有子context都取消

4. context.WithTimeout / context.WithDeadline

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

// 截止时间
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

区别:

  • WithTimeout:相对时间(从现在开始多久)
  • WithDeadline:绝对时间(到什么时候)

超时控制实战

HTTP请求超时

go 复制代码
func fetchURL(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    return io.ReadAll(resp.Body)
}

// 使用
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    data, err := fetchURL(ctx, "https://example.com")
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("请求超时")
        } else {
            fmt.Println("请求失败:", err)
        }
        return
    }
    fmt.Println(string(data))
}

数据库查询超时

go 复制代码
func queryUser(ctx context.Context, db *sql.DB, userID int) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    
    var user User
    err := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", userID).
        Scan(&user.ID, &user.Name, &user.Email)
    
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("查询超时")
        }
        return nil, err
    }
    return &user, nil
}

并发请求统一超时

go 复制代码
func fetchAll(ctx context.Context, urls []string) ([][]byte, error) {
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    results := make([][]byte, len(urls))
    errs := make([]error, len(urls))
    var wg sync.WaitGroup
    
    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            results[i], errs[i] = fetchURL(ctx, url)
        }(i, url)
    }
    
    wg.Wait()
    
    // 检查是否超时
    if ctx.Err() != nil {
        return nil, ctx.Err()
    }
    
    // 检查各个请求的错误
    for _, err := range errs {
        if err != nil {
            return nil, err
        }
    }
    
    return results, nil
}

context传值

context.WithValue

go 复制代码
type contextKey string

const (
    userIDKey  contextKey = "userID"
    traceIDKey contextKey = "traceID"
)

func WithUserID(ctx context.Context, userID int) context.Context {
    return context.WithValue(ctx, userIDKey, userID)
}

func UserIDFromContext(ctx context.Context) (int, bool) {
    userID, ok := ctx.Value(userIDKey).(int)
    return userID, ok
}

// 使用
func handleRequest(ctx context.Context) {
    ctx = WithUserID(ctx, 12345)
    processOrder(ctx)
}

func processOrder(ctx context.Context) {
    userID, ok := UserIDFromContext(ctx)
    if !ok {
        // 没有userID
        return
    }
    fmt.Println("处理用户", userID, "的订单")
}

最佳实践

  • key用自定义类型,避免冲突
  • 不要用context传业务数据,只传请求范围的元数据(TraceID、UserID等)
  • 提供封装函数,不要直接暴露key

链路追踪示例

go 复制代码
type contextKey string

const traceIDKey contextKey = "traceID"

func WithTraceID(ctx context.Context) context.Context {
    traceID := uuid.New().String()
    return context.WithValue(ctx, traceIDKey, traceID)
}

func TraceID(ctx context.Context) string {
    if traceID, ok := ctx.Value(traceIDKey).(string); ok {
        return traceID
    }
    return "unknown"
}

// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := WithTraceID(r.Context())
        
        log.Printf("[%s] %s %s 开始", TraceID(ctx), r.Method, r.URL.Path)
        
        start := time.Now()
        next.ServeHTTP(w, r.WithContext(ctx))
        
        log.Printf("[%s] %s %s 完成 耗时:%v", 
            TraceID(ctx), r.Method, r.URL.Path, time.Since(start))
    })
}

// 业务代码中使用
func queryDB(ctx context.Context) {
    log.Printf("[%s] 开始查询数据库", TraceID(ctx))
    // ...
}

context在各层的传递

一个典型的Web服务:

go 复制代码
// Handler层
func GetUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    userID := chi.URLParam(r, "id")
    
    user, err := userService.GetUser(ctx, userID)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    
    json.NewEncoder(w).Encode(user)
}

// Service层
func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 检查缓存
    user, err := s.cache.Get(ctx, userID)
    if err == nil {
        return user, nil
    }
    
    // 查数据库
    user, err = s.repo.FindByID(ctx, userID)
    if err != nil {
        return nil, err
    }
    
    // 写缓存
    _ = s.cache.Set(ctx, userID, user)
    
    return user, nil
}

// Repository层
func (r *UserRepository) FindByID(ctx context.Context, userID string) (*User, error) {
    var user User
    err := r.db.QueryRowContext(ctx, 
        "SELECT id, name, email FROM users WHERE id = ?", userID).
        Scan(&user.ID, &user.Name, &user.Email)
    return &user, err
}

// Cache层
func (c *Cache) Get(ctx context.Context, key string) (*User, error) {
    val, err := c.redis.Get(ctx, key).Result()
    if err != nil {
        return nil, err
    }
    
    var user User
    json.Unmarshal([]byte(val), &user)
    return &user, nil
}

规范:context作为第一个参数,命名为ctx。


常见错误

1. 忘记cancel

go 复制代码
// 错误:会泄漏
func bad() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // ...
}

// 正确
func good() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    // ...
}

2. 把context存到struct里

go 复制代码
// 错误
type Service struct {
    ctx context.Context  // 不要这样!
}

// 正确:context作为方法参数传递
type Service struct {}

func (s *Service) DoSomething(ctx context.Context) error {
    // ...
}

context是请求级别的,不应该存储。

3. 使用context.Value传业务数据

go 复制代码
// 错误:不要用context传业务参数
ctx = context.WithValue(ctx, "order", order)

func processOrder(ctx context.Context) {
    order := ctx.Value("order").(Order)
    // ...
}

// 正确:业务数据走参数
func processOrder(ctx context.Context, order Order) {
    // ...
}

4. 不检查context是否取消

go 复制代码
// 错误:长循环不检查ctx
func process(ctx context.Context, items []Item) {
    for _, item := range items {
        doHeavyWork(item)  // 即使ctx取消了也继续执行
    }
}

// 正确
func process(ctx context.Context, items []Item) error {
    for _, item := range items {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
        doHeavyWork(item)
    }
    return nil
}

context的继承关系

scss 复制代码
Background (根)
    │
    ├── WithCancel ────────────┐
    │       │                  │ cancel() 会取消所有子context
    │       ├── WithTimeout    │
    │       │       │          │
    │       │       └── WithValue
    │       │
    │       └── WithValue
    │
    └── WithTimeout
            │
            └── WithValue

特点:

  1. 取消向下传播,父取消,子都取消
  2. 取消不向上传播,子取消,父不受影响
  3. Value沿着链向上查找
go 复制代码
func main() {
    ctx1 := context.Background()
    ctx2, cancel2 := context.WithCancel(ctx1)
    ctx3, cancel3 := context.WithTimeout(ctx2, time.Second)
    ctx4 := context.WithValue(ctx3, "key", "value")
    
    // cancel2() 会同时取消 ctx2, ctx3, ctx4
    // cancel3() 只取消 ctx3, ctx4
    // ctx4.Value("key") 能取到
    // ctx3.Value("key") 取不到(Value不向上传播)
    
    defer cancel2()
    defer cancel3()
}

超时时间的设计

经验值:

go 复制代码
// HTTP对外接口总超时
const APITimeout = 30 * time.Second

// 单个下游服务调用
const ServiceTimeout = 5 * time.Second

// 数据库查询
const DBTimeout = 3 * time.Second

// Redis操作
const CacheTimeout = 100 * time.Millisecond

超时递减

go 复制代码
func handleRequest(ctx context.Context) {
    // 请求总超时30s
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    // 查缓存,100ms
    data, err := queryCache(ctx)
    if err == nil {
        return data
    }
    
    // 查库,3s
    // 注意:这里不要再创建新的timeout context
    // 因为剩余时间可能不足3s了,用父context会自动继承剩余时间
    data, err = queryDB(ctx)
    if err != nil {
        return err
    }
    
    return data
}

总结

context的三个核心功能:

  1. 取消信号传播:cancel()
  2. 超时控制:WithTimeout/WithDeadline
  3. 请求级数据传递:WithValue

使用规范:

  • context作为第一个参数
  • 不要存到struct里
  • cancel必须调用
  • WithValue只传元数据,不传业务数据
  • 长操作要检查ctx.Done()

记住一句话:context是请求的生命周期控制器


有问题评论区聊。

相关推荐
计算机学姐2 小时前
基于SpringBoot的智能家教服务平台【2026最新】
java·spring boot·后端·mysql·spring·java-ee·intellij-idea
思成Codes2 小时前
Go 语言中数组与切片的本质区别
开发语言·后端·golang
superman超哥3 小时前
Rust Trait约束(Trait Bounds):类型能力的精确契约
开发语言·后端·rust·rust trait约束·trait bounds·类型能力·精确契约
superman超哥3 小时前
Rust Where子句的语法:复杂约束的优雅表达
开发语言·后端·rust·rust where子句·复杂约束·优雅表达
xiaok3 小时前
使用docker部署koa+sql server+react项目完整过程
后端
SadSunset3 小时前
(44)Spring6集成MyBatis3.5(了解即可,大部分用springboot)
java·spring boot·后端
武子康3 小时前
大数据-199 决策树模型详解:节点结构、条件概率视角与香农熵计算
大数据·后端·机器学习
IT_陈寒3 小时前
Python 3.12 性能优化:5 个鲜为人知但提升显著的技巧让你的代码快如闪电
前端·人工智能·后端
短剑重铸之日3 小时前
《深入解析JVM》第四章:JVM 调优
java·jvm·后端·面试·架构