网关合并逻辑必须在入口中间件层实现,而非handler或singleflight:需语义归一化key、滑动时间窗口flush、独立context与header/body隔离还原。

不能靠学完语法就去写网关合并逻辑——高并发下的数据合并不是“多个goroutine跑完再collect”,而是必须在请求入口层就做语义归一、时间窗口控制和上下文穿透。
为什么golang.org/x/sync/singleflight在网关合并场景下会失效
它设计目标是防缓存击穿,不是请求合并。三个硬伤直接卡死网关:
-
singleflight.Do的key只能是string,但/api/user?id=123&format=json和/api/user?id=123&format=xml语义不同,强行哈希成同一key会导致响应格式错乱 - 返回值固定为
interface{},没法按原始请求的Accept头动态序列化成application/json或text/xml - 没有时间窗口机制,所有同key请求排队等第一个完成,10ms内进来的50个请求可能被拖到300ms才返回,放大长尾延迟
合并必须发生在网关入口,而不是handler里
一旦进到handler,context已绑定单个请求生命周期,goroutine泄漏、超时无法传递、header/body差异无法还原。正确做法是在http.Handler中间件层拦截:
- 提取路径+归一化query(如把
id=1&id=2和id=2&id=1标准化为id=1,2)生成key - 用
map[string]*mergeBatch管理批次,key对应滑动窗口内的待合并请求 - 每个
mergeBatch持有一个time.Timer,10ms后自动flush,避免等待过久 - 原始请求的
Header、Body需提前拷贝并存入batch,后续聚合调用时才能还原响应
http.ServeMux根本不能用来做合并
它只做字符串前缀匹配路由,不持有任何请求上下文,也不支持并发协调。有人试图在里面塞sync.WaitGroup或chan,结果必现:
- goroutine泄漏:某个请求超时退出,但WaitGroup没done,后续请求永远阻塞
- 超时错乱:多个请求共用一个
context.WithTimeout,一个提前取消,其余全被误杀 - 竞态写入:多个goroutine并发往同一个slice append,没加锁导致panic或数据错位
真实网关合并的最小可行结构
不是写个demo,而是要能扛住每秒上千合并批次。关键点只有三个:
- key生成必须包含路径+归一化query hash+accept type签名,例如
"/api/user:sha256(id=1,2):json" - 每个batch绑定独立
context.Context,超时从入口传入,且cancel函数注册到batch cleanup中 - body读取必须用
io.LimitReader设上限,防止恶意大body拖垮内存;query参数解析要用url.Values而非手动split,避免id=a%2Cb被误拆
最易被忽略的是:合并后回写响应时,必须按每个原始请求的Content-Type和Accept-Encoding分别encode,不能统一用JSON返回——这点在压测时才会暴露,前端报406错误却查不到日志源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











