Beego 无开箱即用限流中间件,需用 beego.InsertFilter 结合 rate.Limiter 实现 per-IP 限流,并注意并发安全、路径匹配、状态持久化及阈值科学设定。

Beego 本身不提供开箱即用的接口限流中间件,必须自己基于 beego.InsertFilter + 第三方限流库(如 golang.org/x/time/rate)实现,否则所有“限流”配置都是假的。
为什么 beego.LimitFilter 不可用
旧版 Beego(v1.x)曾有实验性 beego.LimitFilter,但 v2.x 已彻底移除;当前任何文档里提到它,基本是过时内容或误传。直接调用会报 undefined: beego.LimitFilter 错误。
- Beego v2.3.5 及之后版本无内置限流能力,官方明确建议自行封装
- 试图通过配置文件(如
app.conf)写LimitQPS = 10完全无效 —— 框架根本不会读取或处理该字段 - 社区常见错误:把
beego.InsertFilter和路由路径写错,比如用"/api/*"却漏掉末尾星号,导致过滤器不触发
用 rate.Limiter 实现 per-IP 限流
最常用、轻量且可控的方式:在 BeforeRouter 阶段对每个请求 IP 做令牌桶判断。注意必须手动维护 map[string]*rate.Limiter 并加锁,因为 Go map 非并发安全。
- 不要在每次请求都新建
rate.NewLimiter,否则内存泄漏 + GC 压力大 - IP 获取要用
c.Ctx.Input.IP(),不是c.Ctx.Request.RemoteAddr(后者含端口,且可能被代理污染) - 限流后建议返回
429 Too Many Requests,并加X-RateLimit-Remaining头便于前端感知
示例片段:
var (
ipLimiters = sync.Map{} // key: ip string, value: *rate.Limiter
limiterR = rate.Every(1 * time.Second)
)
<p>func IPRateLimitFilter(ctx <em>context.Context) {
ip := ctx.Input.IP()
limiter, _ := ipLimiters.LoadOrStore(ip, rate.NewLimiter(limiterR, 3))
if !limiter.(</em>rate.Limiter).Allow() {
ctx.Output.SetStatus(429)
ctx.Output.Body([]byte("too many requests"))
return
}
}</p><p>// 注册
beego.InsertFilter("/*", beego.BeforeRouter, IPRateLimitFilter)
按路由路径做分级限流
不同接口重要性不同,比如登录接口应比公开文章列表更宽松。可结合 beego.NSFilter 对命名空间单独设限,避免全局一刀切。
- 用
beego.NewNamespace("/api/v1")包裹一组路由,再对其挂载专属限流过滤器 - 避免在
BeforeExec阶段限流 —— 此时 Controller 已实例化、反射已发生,浪费资源 - 若需区分用户身份(如 JWT 中的
user_id),得先解析 token 再取 ID,不能依赖 session(session 可能未初始化)
关键点:路径匹配优先级高于过滤器注册顺序,NSFilter 的路径前缀必须和 NSGet/NSPost 定义一致,否则不生效。
容易被忽略的并发与持久化问题
限流状态默认只存在内存里,重启服务就清零。生产环境若需跨实例共享配额,必须对接 Redis + Lua 脚本(如 INCR + EXPIRE 组合),不能靠本地 map。
- 单机限流够用?看 QPS:低于 500 可用本地
sync.Map;超 1000 建议上 Redis - Redis 方案别用
SETNX+ 单独EXPIRE—— 两步非原子,高并发下会漏控 - Beego 的
context.Context生命周期短,别在里面存限流状态对象,会逃逸到堆且无法复用
真正卡住人的往往不是怎么写逻辑,而是没意识到限流阈值该按“峰值流量 × 安全系数”算,而不是拍脑袋填个 10。


















