echo.RateLimiter不能限制并发数,因其基于令牌桶实现速率限制,只控单位时间请求数,不感知当前活跃请求;真限并发需用计数器+锁或带缓冲channel控制同时执行的goroutine数。

为什么 echo.RateLimiter 不能直接限制并发数
很多人看到 echo.RateLimiter 就以为能控并发,其实它只做「单位时间请求数」限制(即速率限制),底层依赖 golang.org/x/time/rate.Limiter,本质是令牌桶——对突发流量友好,但完全不感知当前有多少请求正在执行。真正限制“同时处理的请求数”,得靠计数器 + 互斥锁或带容量的 channel 控制活跃 goroutine 数量。
用 sync.WaitGroup + sync.Mutex 实现简易并发限流中间件
这是最可控、无额外依赖的方式,适合中低并发场景(比如限制最多 50 个请求同时处理)。关键点在于:在请求进入 handler 前加锁计数,离开时减数,并在超限时直接返回 http.StatusTooManyRequests。
常见错误现象:panic: sync: negative WaitGroup counter —— 没配对调用 Done(),尤其在提前 return 或 panic 路径里漏了 wg.Done()。
实操建议:
- 用
sync.Mutex保护计数器,避免竞态;sync.WaitGroup不适合做计数器,仅用于等待,这里用普通int+Mutex更清晰 - 务必在
defer中完成计数释放,覆盖所有退出路径(包括 error return、panic) - 不要在中间件里阻塞等待空闲 slot,那会把连接池拖垮;应快速失败
示例中间件:
func ConcurrencyLimiter(max int) echo.MiddlewareFunc {
var (
mu sync.Mutex
count int
)
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
mu.Lock()
if count >= max {
mu.Unlock()
return echo.NewHTTPError(http.StatusTooManyRequests, "concurrent limit exceeded")
}
count++
mu.Unlock()
<pre class="brush:php;toolbar:false;"> defer func() {
mu.Lock()
count--
mu.Unlock()
}()
return next.ServeHTTP(c)
})
}}
用带缓冲的 channel 实现更轻量的并发控制
利用 chan struct{} 的阻塞特性天然实现“获取 slot → 执行 → 归还”流程,比手写锁更简洁,且自带队列语义(可设缓冲区大小为 0 实现纯抢占,或 >0 实现短时排队)。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
使用场景:希望代码更函数式、减少显式锁逻辑;或需要和 context 超时配合(例如带 timeout 的 acquire)。
参数差异与坑点:
- channel 容量 = 最大并发数,
make(chan struct{}, 10)表示最多 10 个请求并发执行 - 若用
select+time.After实现 acquire 超时,注意别让ctx.Done()和超时同时触发导致重复 close 或 goroutine 泄漏 - 必须确保每个
acquire都对应一次release,否则 channel 会满,后续所有请求永久阻塞
简版示例(无超时):
func NewConcurrencyLimiter(max int) echo.MiddlewareFunc {
sem := make(chan struct{}, max)
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
select {
case sem return next.ServeHTTP(c)
})
}
}
生产环境要注意的三个细节
真实服务里,单纯限制 handler 入口并发还不够。容易被忽略的是:
-
echo.HTTPErrorHandler本身也可能被高频触发,如果错误处理逻辑重(比如写日志到磁盘、调远程服务),它自己也会成为瓶颈,需单独限流或异步化 - 长连接(如 WebSocket、Server-Sent Events)会持续占用一个 goroutine,但不走常规 HTTP middleware 流程,得在 upgrade 后手动控制连接总数
- 如果用了
echo.WithHTTPErrorHandler自定义错误处理器,要确认它内部没隐式启动 goroutine 或阻塞 I/O,否则会绕过你的并发控制
并发控制不是加一层中间件就完事,得看住从连接建立、路由分发、handler 执行到错误兜底的整条链路里,哪些环节真正消耗 worker goroutine。










