rate.limiter 必须全局复用或按 key 缓存(用 sync.map 或读写锁),key 需带维度标识并定期清理;wait 必须带超时,健康接口跳过限流;分布式限流需 redis+lua,多实例下本地限流无效;真实难点在于 ip 提取准确、配置热加载安全及 redis 故障降级。

rate.Limiter 实例不能在 handler 里 new
每次请求都调用 rate.NewLimiter(10, 5),等于为每个请求新建一个独立令牌桶——100 并发就产生 100 个桶,总放行能力变成 1000 QPS,限流完全失效。
- 正确做法是全局复用(如全局限流)或按 key 缓存(如
"ip:" + realIP、"user:" + userID) - 缓存必须用
sync.Map或带读写锁的 map;普通 map 在高并发下会 panic - key 命名要带区分维度,避免所有流量打到同一个桶里
- 别忘了清理闲置 key:对每个新 key 启动
time.AfterFunc(30 * time.Minute, func() { syncMap.Delete(key) })
HTTP 中间件里 Wait(ctx) 必须带超时
limiter.Allow() 是非阻塞的,返回 false 后若继续执行后续逻辑,请求仍会透传;limiter.Wait(ctx) 虽能等待,但没设超时就会永久挂起,拖垮整个 goroutine。
- 推荐统一用
limiter.Wait(ctx),且 ctx 必须来自r.Context()并套一层context.WithTimeout(, 100*time.Millisecond) - 健康检查接口(如
/health)务必跳过限流,否则 k8s probe 会反复失败 - 若需返回标准限流头(
X-RateLimit-Limit等),得用limiter.ReserveN(now, 1)预占再算剩余,直接减法在并发下不准
多实例部署必须用 Redis+Lua 做分布式限流
rate.Limiter 只在当前进程内存生效,5 个 Pod 各跑一个 rate.NewLimiter(100, 200),入口总流量就是 500 QPS —— 这不是 bug,是设计使然。
- 跨实例限流必须依赖共享存储,Redis 是事实标准,etcd 次之
- 必须用 Lua 脚本封装
INCR+EXPIRE+GET,拆成多次网络调用会因竞态导致计数错误 - key 命名要细粒度,比如
"limit:svc:user-get:12345",而不是笼统的"user-get" - 连接池要调大(
PoolSize: 50),超时设短(Timeout: 100ms),否则限流逻辑本身成瓶颈
真实难点不在算法,而在提取和降级
真正难的不是写通限流逻辑,而是让 key 提取不漏(比如 X-Real-IP 没配 TrustedProxies 就拿不到真实 IP)、让配置热加载不 panic(reload 时 map 正在被读又同时被写)、还有 Redis 故障时的降级策略。
- 提取真实 IP 必须校验可信代理链,否则限流对象可能是 Nginx 内网地址
- 配置变更要用
sync.RWMutex保护 limiter map,reload 期间禁止写入 - Redis 不可用时,要么降级为本地限流(容忍一定误差),要么直接放行(避免雪崩),不能阻塞主流程
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











