最直接有效的方式是用带缓冲的 channel 实现信号量:make(chan struct{}, N) 创建容量为 N 的许可池,每次请求 select 获取空 struct{},处理完归还。

Go 里限制并发请求数量,最直接有效的方式是用 semaphore(信号量)或 channel 控制并发槽位,而不是靠 time.Sleep 或粗粒度锁——后者要么不准,要么伤性能。
用 chan struct{} 实现轻量级并发计数器
这是最常用、零依赖、语义清晰的做法:用带缓冲的 channel 当“许可池”,每次请求先尝试取一个空 struct,处理完再归还。
-
make(chan struct{}, N)创建容量为N的 channel,即最大并发数 - 获取许可:
select { case sem ,阻塞直到有空槽 - 释放许可:
或sem 都不对,正确是close(sem); sem = make(...)不推荐;应统一用归还(注意是接收操作) - 别在 defer 里写
,万一 channel 已被关闭会 panic;建议显式放在 handler 尾部或用 recover 包裹
用 golang.org/x/sync/semaphore 做生产级限流
官方扩展包提供的 semaphore.Weighted 支持带权重的获取(比如大文件上传占 3 个 slot),也支持上下文取消和超时,比裸 channel 更健壮。
- 初始化:
sem := semaphore.NewWeighted(int64(N)) - 获取:
err := sem.Acquire(ctx, 1),返回 error 可判断是否超时或取消 - 释放:
sem.Release(1),必须与 Acquire 的 weight 匹配 - 注意:
Acquire是阻塞的,但若 ctx 已 cancel,则立即返回 error;不要忽略这个 error,否则请求会卡住
HTTP 中间件里嵌入限流逻辑的常见陷阱
很多人把限流逻辑写在 middleware 里,却忽略了中间件生命周期和 handler 并发模型的关系。
立即学习“go语言免费学习笔记(深入)”;
- 全局共用一个
semaphore.Weighted实例没问题,但不能每个请求都 new 一个 - 别在 middleware 中直接调用
Acquire后就 next.ServeHTTP —— 如果 handler panic,Release永远不会执行,导致“许可证泄漏” - 正确姿势:用
defer包一层Release,但要确保它只在 acquire 成功后注册,例如:if err := sem.Acquire(r.Context(), 1); err != nil { http.Error(w, "too many requests", http.StatusTooManyRequests); return } defer sem.Release(1) - 如果 handler 里还起了 goroutine(比如异步日志、消息推送),要确认这些 goroutine 不依赖该请求的上下文或未释放资源,否则可能引发竞态
真正难的不是写出让并发数不超标的代码,而是让限流行为在 panic、context cancel、网络中断、handler 提前 return 等各种边界下依然可预测——这时候裸 channel 容易漏 release,semaphore.Weighted 的 error 分类和 context 绑定就显得必要了。










