应为每个ip或userid创建独立rate.limiter实例并用sync.map缓存,提取ip需校验可信代理与合法性,userid限流必须在认证后进行,key建议组合为“ip:userid”,高并发时需分片或lru降级,并定期清理过期实例。

用 golang.org/x/time/rate 做基础限流,但别直接套用
这个包的 Limiter 是单实例、内存级、无状态的,适合单请求内节流(比如防止 goroutine 疯狂重试),但没法跨请求识别 IP 或 UserID。硬塞进去会导致所有用户共用一个桶,完全失效。
正确做法是把它当“原子部件”用:每个 IP 或每个 UserID 对应一个独立的 rate.Limiter 实例,再用 map 或 sync.Map 缓存。注意别用普通 map 并发读写——会 panic。
- 用
sync.Map存map[string]*rate.Limiter,key 是IP或拼接后的IP:UserID - 限流器创建后不要频繁重建,复用已有实例;可用
time.Now()+ TTL 清理过期项 - 避免在 HTTP handler 里每次都调
rate.NewLimiter,开销不小,尤其 QPS 高时
提取 IP 要防伪造,X-Forwarded-For 不可信
直接读 r.RemoteAddr 在直连场景下能拿到真实 IP,但前面有 Nginx / ELB / CDN 时就只剩代理地址了。很多人盲目信任 X-Forwarded-For,结果被恶意 header 绕过限流。
必须结合 TrustedProxies 配置做逐层剥离。Gin 框架可设 engine.ForwardedByClientIP = true 并配 engine.SetTrustedProxies;原生 net/http 则得自己解析 X-Forwarded-For,从右往左数指定跳数。
立即学习“go语言免费学习笔记(深入)”;
- 若你的服务只挂在一台可信 Nginx 后,取
X-Forwarded-For的倒数第二个字段(最后一个可能是攻击者伪造的) - 用
net.ParseIP()校验提取出的 IP 是否合法,非法值统一归为"0.0.0.0"或丢弃 - IPv6 地址带冒号和方括号(如
[::1]),用net.ParseIP解析比字符串切分更稳
UserID 限流要和认证绑定,不能依赖前端传参
从 URL 或 header 读 UserID 是危险的——没鉴权就限流,等于把开关交给客户端。必须确保该 ID 已在 JWT / session / OAuth2 中完成校验且不可篡改。
典型错误是:先限流,再校验 token;或者用前端传的 user_id=123 当 key。一旦 token 过期或伪造,限流策略就形同虚设。
- 限流逻辑必须放在认证中间件之后,用 context.Value 透传已解码的
userID - key 建议组合为
fmt.Sprintf("%s:%s", ip, userID),避免 IP 和 UserID 冲突(比如 IP 是"123",UserID 也是"123") - 敏感操作(如登录、密码重置)建议额外叠加设备指纹或行为特征,单靠 UserID 容易被撞库绕过
高并发下 sync.Map 会成为瓶颈,得降级或分片
sync.Map 在 key 数量少、读多写少时表现好,但当每秒新建几千个新 IP 的限流器(比如爬虫洪峰),它的扩容和哈希冲突会让 P99 延迟飙升。
这时要么加一层 LRU 缓存控制总实例数(比如最多存 10 万活跃 IP),要么改用分片 map:按 IP 哈希取模到 16 个子 map,写操作分散到不同锁上。
- 用
hash/fnv对 IP 做一致性哈希,比%更均匀,扩缩容时迁移 key 更少 - 设置
maxLimiters = 100000,超限时用固定 fallback 限流器(比如全局 100rps),别 panic 或阻塞 - 别忘了定期清理:启动 goroutine 每分钟扫一次,删掉 5 分钟没访问的
rate.Limiter
真正难的不是写对一个限流函数,而是让 IP 提取不被绕过、UserID 不脱离认证上下文、缓存结构扛住突增流量——这些地方一松动,整个限流就漏了。










