直接用net/http做聚合会出问题,因其默认不支持超时传递、上下文取消传播和并发请求统一生命周期管理,导致下游卡住时整个响应hang住,或无法及时中止仍在运行的请求,引发资源泄漏与响应延迟不可控。

为什么直接用 net/http 做聚合会出问题
因为 net/http 的 http.Client 默认不支持超时传递、上下文取消传播和并发请求的统一生命周期管理。你手动并发发多个请求,但某个下游卡住,整个聚合响应就 hang 住;或者一个子请求返回了 401,你却没法让其他还在跑的请求立刻中止——这会导致资源泄漏和响应延迟不可控。
真正能用的起点是 context.Context 驱动的并发控制 + 显式错误分类处理:
- 所有子请求必须带上同一个
ctx,且使用带超时的context.WithTimeout - 用
errgroup.Group(来自golang.org/x/sync/errgroup)替代裸sync.WaitGroup,它自动传播第一个错误并等待全部完成 - 别在 handler 里直接
json.Marshal所有响应体——不同服务返回结构不同,需要先统一解包再按字段映射
如何用 errgroup 控制并发子请求并收集结果
errgroup.Group 是 Go 生态里最轻量、最贴近标准库风格的聚合控制工具。它不侵入业务逻辑,只负责“等全部完成 or 第一个失败”,且天然兼容 context。
典型写法:
eg, ctx := errgroup.WithContext(r.Context())
var respA, respB MyServiceAResponse
var errA, errB error
eg.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.a.com/user", nil)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(&respA)
})
eg.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.b.com/order", nil)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(&respB)
})
if err := eg.Wait(); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
// 此时 respA 和 respB 已就绪,可组合返回
注意:eg.Wait() 返回的是第一个非 nil 错误,但所有 goroutine 仍会继续执行完(或被 context 取消)。如果下游服务响应慢但最终成功,你依然能拿到部分结果——这正是网关聚合该有的容错行为。
响应拆分时怎么避免字段名冲突和类型不一致
聚合后响应不是简单拼 JSON 字符串。两个服务都返回 {"id": 123},但含义不同(一个是用户 ID,一个是订单 ID),直接合并会覆盖。
安全做法是显式定义聚合响应结构体,并为每个子响应指定嵌套路径:
type AggregatedResponse struct {
User *UserResponse `json:"user,omitempty"`
Order *OrderResponse `json:"order,omitempty"`
Time int64 `json:"timestamp"`
}
关键点:
- 字段用指针类型(
*UserResponse),这样空值不会序列化成{},而是省略整个字段 - 不要依赖下游的
jsontag,自己定义字段名和嵌套层级,避免命名污染 - 如果某个子请求失败但你想保留部分成功结果,就不要把整个聚合响应设为失败,而是在对应字段填
nil,并在顶层加"errors": {"user": "timeout"}
别用 map[string]interface{} 动态拼接——它会让后续消费方无法做静态校验,也容易因 key 写错引发 panic。
生产环境必须处理的三个隐形陷阱
本地跑通不等于线上可用。这三个点几乎每个团队都踩过:
-
http.Client缺少连接池配置:默认MaxIdleConnsPerHost = 2,高并发下大量新建 TCP 连接,触发too many open files。必须显式设置Transport,比如MaxIdleConnsPerHost: 100 - 下游返回非 JSON 或
Content-Type: text/plain,但你的json.Decoder直接 panic。应在 decode 前检查resp.Header.Get("Content-Type"),或用io.LimitReader防止大响应体 OOM - 网关自身没做限流,单个恶意请求触发 10+ 子请求,瞬间打爆下游。至少在
errgroup外层加semaphore(如golang.org/x/sync/semaphore)限制并发数
真正的难点不在代码怎么写,而在怎么界定“哪个错误该透传、哪个该降级、哪个该重试”。这部分没法靠框架解决,得结合业务 SLA 来配策略。











