http.ServeMux不能用于请求合并,因其仅负责路由分发,不维护请求上下文、不支持并发控制、也不提供合并逻辑钩子;强行用sync.WaitGroup或chan易致goroutine泄漏和超时错乱。

为什么不能直接用 http.ServeMux 做请求合并
因为 http.ServeMux 只做路由分发,不维护请求上下文、不支持并发控制、也不提供合并逻辑钩子。你一上来就往里面塞 sync.WaitGroup 或 chan,很容易触发 goroutine 泄漏或超时错乱。
真实场景中,合并必须发生在「同一路径 + 相近时间窗口 + 可聚合参数」三个条件同时满足时。比如多个 /api/user?id=123 和 /api/user?id=456 请求,在 10ms 内到达,应该被聚合成一次后端调用 GET /users?id=123,456。
- 合并时机必须由网关层显式控制,不能依赖 handler 自行协调
- 每个合并批次需绑定独立的
context.Context,超时和取消必须穿透到底层 - 原始请求的 header、query、body 差异要提前归一化,否则合并后无法还原响应
golang.org/x/sync/singleflight 不适合网关合并的三个原因
它解决的是「缓存击穿」,不是「请求合并」。在网关场景下强行套用会出问题:
- 键(key)只支持 string,无法表达 query 参数组合的语义(比如
id=1&format=json和id=1&format=xml必须视为不同请求) - 返回值是
interface{},没法按原始请求的Acceptheader 动态序列化 - 没有时间窗口控制——所有同 key 请求都会排队等第一个完成,导致长尾延迟放大
正确做法是自己实现带滑动窗口的合并池,用 map[string]*mergeBatch 管理,key 由路径 + 归一化 query hash 生成,value 带 time.Timer 控制 flush 时机。
立即学习“go语言免费学习笔记(深入)”;
如何安全地把多个 HTTP 请求合并成一个后端调用
关键不是“怎么发”,而是“怎么拆”:前端发来的每个请求都得能从合并后的响应里精准提取自己的那部分数据。
- 后端服务必须支持批量接口,例如把
GET /user?id=1改为POST /users/batch,body 是{"ids": ["1","2","3"]} - 网关在合并前,对每个原始请求提取唯一 trace ID,并存入
context.WithValue();响应回来后,按 ID 分发到对应 responseWriter - 避免用
json.Unmarshal全量解析再切片——直接用encoding/json.Decoder流式读取数组,边解码边匹配 ID
示例合并逻辑片段:
batch := &mergeBatch{
key: hashPathAndQuery(req),
reqs: []*pendingReq{{req, w, ctx}},
timer: time.AfterFunc(5*time.Millisecond, b.flush),
}
合并失败时如何不丢请求也不卡死
合并不是必选项,而是一种优化策略。一旦失败(如后端 batch 接口返回 400、超时、或合并池满),必须降级为单条转发。
- 每个 pending 请求自带 fallback timer,比 batch timer 提前 1ms 触发,触发时从 batch 中移除并单独转发
- 合并池大小要限流,用
semaphore.NewWeighted(100)控制并发合并批次,防止单个热点路径耗尽内存 - 记录
merge_attempt_total和merge_fallback_total指标,当 fallback 率 > 5%,自动关闭该路径的合并开关
真正难的不是写合并逻辑,而是让 fallback 行为对上游完全透明——header、status code、body、timing 都得和未合并时一致。这点最容易被忽略。



















