gin 不支持自动并发请求下游并合并响应,必须显式用 errgroup.group + context.withtimeout 实现;否则易因上下文丢失、错误处理缺失等导致崩溃。

Gin 本身不提供“自动并发请求下游并合并响应”的能力,所有并发控制必须由开发者显式实现;否则默认是串行调用,哪怕用了 go 关键字也容易因上下文丢失、错误处理缺失或资源未回收而崩溃。
为什么不能直接在 Gin handler 里用 go 启 goroutine 发起 HTTP 请求
看似简单的一句 go http.Get(...) 在 Gin 中极易出问题:
- handler 返回后,
c(*gin.Context)生命周期结束,其绑定的context.Context可能已被取消,但 goroutine 仍在运行,导致“幽灵请求”和内存泄漏 - goroutine 内无法安全调用
c.JSON()或c.Abort(),会 panic - 没有统一错误收集机制,一个下游失败时,其他请求可能还在跑,最终响应结构不一致
- 缺少超时控制,某个下游卡住会拖垮整个 handler
正确做法:用 errgroup.Group + context.WithTimeout
这是 Go 官方推荐的并发协调模式,天然适配 Gin 的 context 生命周期:
func summaryHandler(c *gin.Context) {
// 从 gin.Context 派生带超时的子 context
ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
defer cancel()
var g errgroup.Group
g.SetContext(ctx)
var userRes, orderRes, profileRes []byte
var userErr, orderErr, profileErr error
g.Go(func() error {
resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "http://user-svc/user", nil))
if err != nil {
return err
}
defer resp.Body.Close()
userRes, userErr = io.ReadAll(resp.Body)
return userErr
})
g.Go(func() error {
resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "http://order-svc/order", nil))
if err != nil {
return err
}
defer resp.Body.Close()
orderRes, orderErr = io.ReadAll(resp.Body)
return orderErr
})
g.Go(func() error {
resp, err := http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "http://profile-svc/profile", nil))
if err != nil {
return err
}
defer resp.Body.Close()
profileRes, profileErr = io.ReadAll(resp.Body)
return profileErr
})
// 等待全部完成或任一出错/超时
if err := g.Wait(); err != nil {
c.JSON(502, gin.H{"error": "downstream failed", "detail": err.Error()})
return
}
c.JSON(200, gin.H{
"user": json.RawMessage(userRes),
"order": json.RawMessage(orderRes),
"profile": json.RawMessage(profileRes),
})
}
必须注意的三个坑
即使用了 errgroup,以下三点不处理,线上照样翻车:
-
http.DefaultClient缺少连接池配置:默认MaxIdleConnsPerHost = 2,高并发下会阻塞在 DNS 解析或 TCP 建连阶段。应替换为自定义 client:&http.Client{Transport: &http.Transport{MaxIdleConnsPerHost: 100}} - 下游响应体未限制大小:恶意服务返回 GB 级响应会 OOM。建议用
io.LimitReader(resp.Body, 2 限制最大读取 2MB - JSON 合并时未处理无效 JSON:如果某个下游返回非 JSON(如 500 页面 HTML),
json.RawMessage会原样透传,前端解析失败。应在各Go()分支内提前校验resp.Header.Get("Content-Type")和状态码
真正难的不是并发本身,而是每个下游调用都得单独做超时、限流、重试、熔断、指标打点——这些逻辑一旦散落在 handler 里,三个月后没人敢动。生产环境建议把这类聚合逻辑抽成独立 service 层,用 github.com/sony/gobreaker 或 resilience-go 封装,而不是堆在 Gin 路由函数里。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











