Go context 工程实践:超时、取消与链路追踪

test12026-09-110 次阅读

没有 context 的 HTTP 调用迟早会拖垮服务

Go 里最容易被忽视的隐患,是 goroutine 和下游调用缺少取消信号。一个 HTTP 请求进来,里面查数据库、调第三方接口、读 Redis,只要任意一环卡住且没设超时,这个 goroutine 和它占用的连接就会一直挂着。流量一大,连接池耗尽,整个服务雪崩。context 就是用来在调用链上传递"取消"和"截止时间"的标准机制。

从请求入口带上超时

HTTP handler 拿到的 r.Context() 在客户端断开时会自动取消。我们在它上面叠加一个超时,传给所有下游调用,保证整条链路最多花 2 秒。

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    user, err := queryUser(ctx, r.URL.Query().Get("id"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    _ = json.NewEncoder(w).Encode(user)
}

下游必须消费 ctx 而不是忽略它

只传 ctx 没用,关键是每个 IO 调用都要用带 Context 的版本。下面用 QueryRowContext,数据库驱动会在 ctx 超时或取消时中断查询。

func queryUser(ctx context.Context, id string) (*User, error) {
    row := db.QueryRowContext(ctx,
        "SELECT id,name FROM users WHERE id=$1", id)

    var u User
    if err := row.Scan(&u.ID, &u.Name); err != nil {
        return nil, err
    }
    return &u, nil
}

用值传递贯穿链路标识

context.WithValue 适合放请求级的元数据,比如 traceID。注意 key 用自己的类型,避免和其他包冲突;不要拿它传可选参数,那是反模式。

type ctxKey string
const traceKey ctxKey = "trace"

func withTrace(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, traceKey, id)
}

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

三个容易踩的坑

  • 不要存 ctx 进结构体字段,它应作为第一个参数在函数间流动。
  • cancel 函数一定要 defer 调用,否则 WithCancel/WithTimeout 创建的 goroutine 会泄漏。
  • 超时时间从入口统一设,下游不要各自再套一层更长的超时,否则上层超时先触发,下层白跑。

context 不是语法糖,它是 Go 并发程序里控制资源生命周期的主动脉。把超时、取消、trace 都挂在这条链上,你的服务才能在依赖抖动时优雅降级,而不是悄悄堆积成雪崩。

T

test1

文章作者

没有 context 的 HTTP 调用迟早会拖垮服务 Go 里最容易被忽视的隐患,是 goroutine 和下游调用...

分类
技术
发布时间
2026-09-11
字数
约 1650 字
阅读
0 次