gin-contrib/limiter不能直接限制并发请求数,因为它基于令牌桶/漏桶算法,只控制单位时间请求数(qps),不感知当前正在处理的请求数量;真正限制并发数需用信号量(如semaphore.newweighted)在handler入口处硬控制同时执行的goroutine数量。

为什么 gin-contrib/limiter 不能直接限制“并发请求数”
很多人想用 Gin 做类似 Nginx 的 limit_conn——即同一时刻最多允许 N 个请求在处理中。但标准限流库(比如 gin-contrib/limiter)底层基于令牌桶或漏桶,只管“单位时间请求数”,不管当前有多少请求正在执行。它放行的请求可能堆积在 handler 中,导致 goroutine 爆炸、内存飙升甚至 OOM。
真正限制并发数,核心是控制同时进入 handler 的 goroutine 数量,不是控制入口流量速率。
用 semaphore.NewWeighted 控制并发数最直接
Go 1.19+ 标准库 golang.org/x/sync/semaphore 提供了轻量、无锁的信号量实现,适合做并发数硬限制。它不依赖时间窗口,也不需要外部存储,天然契合“同一时刻最多 N 个请求在处理”的需求。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
semaphore.NewWeighted(int64(n))创建一个容量为n的信号量,每个请求需先Acquire(ctx, 1)才能进入 handler - 必须在 handler 结束前调用
Release(1),否则信号量不会归还,后续请求永久阻塞 - 若请求超时或 context 被 cancel,
Acquire会返回 error,应直接 return,避免占用 slot - 注意:信号量是进程级的,单机有效;集群场景需额外加 Redis 分布式信号量(但开销大,慎用)
// 示例:限制最多 5 个并发请求
var sem = semaphore.NewWeighted(5)
func limitHandler(c *gin.Context) {
if err := sem.Acquire(c.Request.Context(), 1); err != nil {
c.Status(http.StatusTooManyRequests)
return
}
defer sem.Release(1)
// 正常业务逻辑
c.JSON(200, gin.H{"ok": true})
}
别在中间件里用 time.Sleep 或 runtime.Gosched() 模拟限流
有人试图在中间件里计数 + sleep 来“匀速放行”,这既不精确也不安全:
- 计数器没锁保护,高并发下
count++和count--会竞争,导致实际并发远超预期 -
time.Sleep阻塞 goroutine,浪费调度资源;runtime.Gosched()不解决本质问题,只是让出时间片,无法控制并发峰值 - 没有 context 参与,无法响应 cancel 或 timeout,容易拖垮整个服务
- 这种写法看起来像限流,实则绕过了 Go 的并发模型优势,属于反模式
和 gin-contrib/limiter 混用时要注意优先级顺序
如果既要限制总 QPS(比如每秒最多 100 请求),又要限制并发数(比如最多 10 个请求同时执行),两个策略必须分层部署,且并发限制必须更靠近 handler。
- QPS 限流放在外层(如路由前),过滤掉明显过载的请求
- 并发限制放在内层(如 handler 内或紧贴 handler 的中间件),确保进入业务逻辑的 goroutine 数可控
- 切勿把
sem.Acquire放在 QPS 限流中间件之前——否则信号量会被无效请求占满,导致合法请求被饿死 - 错误顺序示例:
QPSLimit → SemAcquire → Handler→ 正确顺序:QPSLimit → Handler → SemAcquire(或更推荐:QPSLimit 中间件 + 单独的并发限制 handler 包裹)
并发数限制真正的难点不在代码长短,而在于释放时机是否 100% 可靠。哪怕一个 panic 没 recover,或者 defer 写错位置,都会让信号量永久泄漏。生产环境务必搭配 pprof 和 goroutine 数监控。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










