rate.limiter 是唯一推荐的单机限流方案,官方实现基于纳秒级单调时钟、线程安全、无 goroutine 开销;必须复用实例、安全解析真实 ip、合理设置 limit/burst、用 allow()/reserven() 而非 wait(),并定期清理 sync.map 中过期条目。

rate.Limiter 是唯一推荐的单机限流方案
别自己手写滑动窗口、别用 time.Ticker 模拟令牌桶、也别用 map[string][]time.Time 做时间戳过滤——这些实现要么 panic,要么 CPU 毛刺,要么精度漂移。官方 golang.org/x/time/rate.Limiter 基于纳秒级单调时钟动态计算令牌,线程安全,Allow() 平均耗时仅 10–50 ns,且无 goroutine 开销。
常见错误是每次请求都 new rate.Limiter(10, 5),结果限流器一建即废;正确做法是复用实例:全局限流用包级变量,按 IP 或用户限流则必须为每个 key 绑定独立 *rate.Limiter 实例。
按 IP 限流必须隔离实例 + 安全解析真实地址
直接用 r.RemoteAddr 当 key 会失效——Nginx、Cloudflare、AWS ALB 都会让所有请求显示同一个地址。必须结合可信代理网段校验真实 IP:
- 先检查请求来源 IP 是否在
trustedProxies = []string{"10.0.0.0/8", "192.168.0.0/16"}内 - 可信时取
X-Forwarded-For最左非私有地址(如192.168.1.100, 203.0.113.5→ 取203.0.113.5) - IP key 推荐用
net.ParseIP(ipStr).To16()转字节数组再转string,避免 IPv4/IPv6 格式不一致导致重复建桶
用 sync.Map[string]*rate.Limiter 缓存,但绝不能只存不删——长期运行后内存持续上涨。建议启动后台 goroutine,每分钟扫描并删除 time.Since(lastAccess) > 30 * time.Minute 的条目。
Limit 和 Burst 参数必须协同设置
rate.NewLimiter(limit rate.Limit, burst int) 两个参数不是独立配置的:
-
limit是长期平均速率(单位:token/秒),推荐用rate.Every(200 * time.Millisecond)(≈ 5 QPS),避免浮点误差导致限速漂移 -
burst是桶容量,也等于首次允许的最大并发数;设为1就退化成死板匀速,设为10表示空闲足够久后可突增 10 次 -
burst必须 ≥limit,否则新速率永远达不到(如limit=10但burst=5,桶最多存 5 个,再快也白搭) - 想容忍短时抖动或重试请求,
burst可设为limit × 2(例如 10 QPS 配burst=20)
动态调速时,必须同时调 SetLimit() 和 SetBurst(),否则旧 burst 会卡住新 limit。
HTTP 中间件里别用 Wait(),优先选 Allow() 或 ReserveN()
Wait() 会阻塞直到拿到令牌或 context 超时,HTTP handler 里用它等于主动卡住请求。绝大多数场景应立刻返回结果:
- 单次请求消耗 1 个令牌:用
if !limiter.Allow() { http.Error(w, "too many requests", 429); return } - 一次请求消耗多个令牌(如上传大文件):必须用
res := limiter.ReserveN(time.Now(), n)判断,若res.OK()为 false 或res.Delay() > 100 * time.Millisecond,直接拒绝 - 切记:
Allow()不重置桶状态,连续调用可能让桶瞬间耗尽;ReserveN()才能精确控制多令牌消耗
真正难处理的不是代码怎么写,而是 IP 解析是否可信、burst 是否设得过小导致误杀正常重试、以及清理逻辑是否真正在跑——这三个点漏掉任何一个,上线后都会变成线上事故。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











