Go标准库net/http无内置限频能力,需手动在handler前插入中间件实现计数、窗口管理与存储,易出现并发不安全或统计遗漏;推荐使用ulule/limiter/v3,其解耦策略与存储,支持内存/Redis后端及令牌桶、滑动窗口算法。

为什么标准库 net/http 无法直接做接口限频
Go 标准库 net/http 本身不提供限频能力,所有请求都会被无差别转发到 handler。你写个 http.HandleFunc,它就只管执行,不会主动查“这个 IP 这分钟发了几次”。想加限频,必须在 handler 执行前插入中间件逻辑,且需自行管理计数、时间窗口、存储(内存 or Redis)、并发安全 —— 这些加起来很容易写出线程不安全或漏统计的 bug。
用 github.com/ulule/limiter/v3 最省事
社区最常用、维护活跃、支持多后端的限频库是 ulule/limiter。它把「策略」和「存储」解耦,能用内存、Redis、Memcached 等,也内置常见算法(如 token bucket 和 sliding window)。默认用内存时,每个进程独立计数,适合单机部署;切 Redis 就能跨实例共享配额。
安装:go get github.com/ulule/limiter/v3
基础用法示例(内存限频):
立即学习“go语言免费学习笔记(深入)”;
import (
"github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/middleware/stdlib"
"github.com/ulule/limiter/v3/drivers/store/memory"
)
store := memory.NewStore()
rate, _ := rate.FromString("100-S") // 每秒最多 100 次
limiter := limiter.New(store, rate)
// 包裹 handler
handler := stdlib.NewMiddleware(limiter).Handler(http.HandlerFunc(yourHandler))
注意:rate.FromString 支持格式如 "10-M"(每分钟 10 次)、"500-H"(每小时 500 次),单位必须大写;S/M/H 后不能有空格。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
按 IP 或用户 ID 限频?关键在 key 生成逻辑
默认 stdlib 中间件用 r.RemoteAddr(即客户端 IP)作为限频 key,但实际中常需更精细控制:比如登录用户走 user_id,未登录用户才走 IP。这时得自己写中间件,而不是直接套 stdlib.NewMiddleware。
自定义 key 的核心是实现 limiter.KeyFunc:
keyFunc := func(r *http.Request) string {
// 优先取 bearer token 解析出 user_id
if uid := getUserIDFromToken(r); uid != "" {
return "user:" + uid
}
// fallback 到 IP(建议去端口,只留 IPv4/IPv6 主体)
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return "ip:" + ip
}
limiter := limiter.New(store, rate, limiter.WithKeyFunc(keyFunc))
-
getUserIDFromToken需你自己实现 JWT 解析或从 session 查,别在这儿做耗时操作 - IP 提取务必用
net.SplitHostPort剥离端口,否则同一客户端不同端口会被算作不同 key - 如果用 Redis store,key 字符串长度尽量短,避免网络开销和 Redis 内存浪费
并发高时内存 store 会丢数据,Redis 不是万能解药
用 memory.NewStore() 在多 goroutine 下是线程安全的,但它是纯内存结构,重启进程就清零;更严重的是:当 QPS 超过几千,频繁读写 map + 定时清理,GC 压力会上升,实测可能拖慢整体吞吐。
换 redis store 能解决持久化和共享问题,但要注意:
- 必须用
drivers/store/redis,不是直接连 redis.Client;它内部用 pipeline 减少 round-trip - Redis 连接池大小要调高(默认 10 太小),否则限频逻辑本身成瓶颈
- 网络延迟会影响限频精度:比如滑动窗口算法在 Redis 中依赖 Lua 脚本,若 RT > 2ms,1000QPS 下可能误放行或误拦截
- 别在限频中间件里做 redis auth 或重连逻辑 —— 失败应降级回内存(或直接 503),不能卡住请求
真正难的不是集成,是压测时发现“限频阈值设 100,实际放过 120”,这种偏差来自算法实现细节(比如 token bucket 的 refill 时机)和存储延迟叠加。上线前一定要用 wrk -t2 -c100 -d30s http://your-api 实测真实漏放率。

















