rate.Limiter必须复用,不可每次请求新建;应定义为包级变量或用sync.Map按用户/IP分桶;Allow()非阻塞、Wait()需带超时context;反向代理下取真实IP须校验X-Forwarded-For首跳;分布式场景需Redis或专用库替代单机限流。

rate.Limiter 必须复用,不能在 handler 里 new
每次请求都调用 rate.NewLimiter,限流就彻底失效——每个实例独立计数,相当于没限。实际压测时,10 QPS 的限制可能被 3 个并发请求就突破。
- 正确做法是定义为包级变量或注入到结构体中,比如:
var globalLimiter = rate.NewLimiter(rate.Limit(10), 5) - 如果按用户 ID 或 IP 分桶,要用
sync.Map存map[string]*rate.Limiter,避免为每个 key 新建实例导致内存暴涨 - 别用闭包捕获局部 limiter 实例——Go 编译器可能优化掉,导致实际复用失败
Allow() 和 Wait() 的行为差异直接影响错误处理逻辑
Allow() 是非阻塞的,只看当前有没有令牌;Wait() 会阻塞直到拿到令牌或 ctx 超时。选错会导致 429 响应不准确,甚至 goroutine 卡死。
- 用
Allow()时,必须立刻返回响应,不能之后再处理业务逻辑——否则刚判断“有令牌”,下一毫秒就被其他 goroutine 抢走 - 用
Wait()必须传入带超时的 context,例如:ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond),否则客户端断连后 goroutine 会永久挂住 - 高频小请求(如心跳)适合
Allow();需保序或配额精确的场景(如支付回调)必须用WaitN(ctx, n)
中间件里提取限流 key 容易忽略 header 伪造和 proxy 转发
想按 IP 限流,直接读 r.RemoteAddr 在反向代理后基本不可靠;想按 API Key 限流,不校验签名就取 header 会被人绕过。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 真实 IP 应优先从
X-Forwarded-For解析,但必须验证首跳是否可信(比如只信任 Nginx 或 Cloudflare 的 IP 段) - API Key 限流前,先做基础校验:
strings.HasPrefix(r.Header.Get("Authorization"), "Bearer "),再解 token 或查 DB - 路径级限流(如
/api/pay)建议用strings.HasPrefix(r.URL.Path, "/api/pay"),别依赖路由框架的匹配结果——中间件执行早于路由解析
分布式部署时单机限流器天然失效
rate.Limiter 是纯内存实现,3 个 Pod 各跑一个实例,用户发 30 QPS 就能绕过 10 QPS 限制。这不是 bug,是设计使然。
立即学习“go语言免费学习笔记(深入)”;
- 小规模服务可接受“近似限流”,但支付、登录等关键接口必须升级:用 Redis + Lua 实现滑动窗口,或引入
uber-go/ratelimit这类支持分布式令牌桶的库 - 若坚持用
golang.org/x/time/rate,只能退化为“每实例限流”,并在入口网关层做全局汇总统计(需额外埋点+聚合) - 别试图用 etcd 或 MySQL 做分布式锁来同步令牌状态——延迟高、吞吐低,反而成为瓶颈
limiter.Wait(),而是判断该在哪一层提取 key、要不要为每个租户单独建桶、以及当流量打穿单机阈值时,你敢不敢承认「这里本来就不该限」。

















