
rate.Limiter.Wait() 必须配合 context 使用,Allow() 不适合 API 限流
很多同学一上来就用 limiter.Allow() 做判断,结果发现限流不生效或超时被忽略——因为 Allow() 是瞬时采样,不阻塞、不等待、不感知 context 超时。真实 API 场景下,你得让请求“等令牌”,而不是“看有没有令牌”。
-
limiter.Wait(ctx)会阻塞直到拿到令牌,或返回context.DeadlineExceeded/context.Canceled - 若服务端设置了 5s 超时,但
limiter.Wait()等了 6s 才返回,那它根本不会进业务逻辑,直接由 gRPC/HTTP 框架回 504 或 429 - 别在
Wait()后再做额外超时判断,那是重复劳动;context 就是干这个的 - 初始化时注意参数含义:
rate.NewLimiter(rate.Every(time.Second), 10)表示“每秒补 10 个令牌,桶初始容量 10”,不是“每秒最多 10 次”——突发流量能立刻通过前 10 个
用户级限流不能只靠单机 rate.Limiter,滑动窗口需自己维护时间戳
按 IP 或用户 ID 限流时,golang.org/x/time/rate 的 Limiter 默认是全局共享的,没法区分主体。想做到“每个用户每分钟最多 100 次”,得自己实现滑动窗口逻辑。
- 用
map[string][]time.Time存每个用户的请求时间戳,配合sync.RWMutex读写安全 - 每次请求进来,先过滤掉
now.Add(-window)之前的旧时间戳,再判断剩余数量是否超限 - 别用固定大小数组模拟窗口——时间精度要求高时(比如毫秒级),slice append + 遍历过滤更可靠
- 内存风险:高频请求用户会积累大量时间戳,建议加个最大长度限制(如最多存 200 条),超出则截断老数据
并发控制别手写 chan struct{},用 semaphore.Weighted 更稳
当你想限制“同时最多跑 3 个导出任务”,有人会写 chan struct{} + len(ch) 判断,但这容易漏释放、panic 后卡死、或误判容量。
- 改用
golang.org/x/sync/semaphore的Weighted:它把 Acquire/Release 封装成带 context 的原子操作 -
sem.Acquire(ctx, 1)可能返回context.Canceled,必须显式检查错误,否则 goroutine 永久挂起 - 务必
defer sem.Release(1),哪怕 handler panic 也能释放;Release 数量必须和 Acquire 完全一致 - Acquire 要在 goroutine 内调用,不是在调度入口处——否则所有任务串行执行,失去并发意义
集群限流必须外接 Redis,本地 rate.Limiter 只能降级兜底
单机 rate.Limiter 在多实例部署下完全失效:10 个 Pod 各自限 100 QPS,实际总流量就是 1000 QPS,后端照样被打垮。
- 分布式限流首选 Redis + Lua 原子脚本,例如用
INCR+EXPIRE组合实现滑动窗口计数 - Go 代码里要封装 fallback 逻辑:Redis 连不上时,自动切到本地
rate.Limiter,避免雪崩 - 别在拦截器里反复 new redis.Client——连接池复用、设置合理 timeout(建议 ≤ 50ms)
- 注意 Redis key 设计:按用户 ID + 时间窗口哈希,避免热 key;比如
rl:uid_123:2026061901
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











