应使用 golang.org/x/sync/semaphore 实现 Go 并发控制,因其安全、语义清晰、支持上下文取消和权重限流,避免手动用 chan 或 Mutex 导致的资源泄漏与逻辑错误。

用 semaphore 包实现 Go 并发请求数量控制
Go 标准库没有内置信号量,但 golang.org/x/sync/semaphore 是官方维护的轻量级实现,比自己用 chan + sync.WaitGroup 更安全、语义更清晰。
常见错误是直接用带缓冲的 chan struct{} 模拟信号量,结果在超时或 panic 场景下忘记释放,导致 goroutine 永久阻塞。
-
semaphore.NewWeighted(10)创建容量为 10 的信号量,注意参数是int64,别传负数或零 - 每次请求前调用
sem.Acquire(ctx, 1),必须检查返回 error —— 如果 ctx 超时或被取消,会返回context.Canceled或context.DeadlineExceeded - 务必用
defer sem.Release(1)保证释放,哪怕 handler panic 也要释放,否则信号量永远卡住
为什么不用 sync.Mutex 或 chan 自己手写限流
sync.Mutex 只能串行化,不是“最多 N 个并发”,而是“同一时间只允许 1 个”。而 chan 手写容易漏掉释放逻辑,尤其在错误分支里。
典型翻车场景:HTTP handler 中用 select 等待 chan,但没处理 ctx.Done(),导致超时请求仍占着 slot 不放。
立即学习“go语言免费学习笔记(深入)”;
-
semaphore内部用sync.Cond实现等待队列,支持按 ctx 取消,天然适配 HTTP 超时控制 - 权重支持(
Acquire(ctx, weight))让你能做“大请求占 2 个 slot,小请求占 1 个”的差异化限流,chan很难干净实现 - 性能上,
semaphore在高竞争下比手动chan更低开销 —— 它避免了 channel 的内存分配和调度唤醒抖动
sem.Acquire 返回 context.Canceled 怎么办
这不是 bug,是设计使然:当请求还没拿到信号量就超时或客户端断开,Acquire 就立刻返回 error,你得立刻返回 HTTP 429 或 503,不能继续往下走。
容易忽略的一点:这个 error 和业务逻辑里的 ctx.Err() 是同一来源,别重复判断。
- 别写
if err != nil { if errors.Is(err, context.Canceled) { ... } }—— 直接if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) - 释放操作只在
Acquire成功后才执行,所以Release前不用判空或加锁 - 如果想记录被拒绝的请求数,只在
Acquire返回非 nil error 时计数,不要在Release后补记
HTTP 中间件里嵌入信号量要注意什么
把 semaphore 当成全局变量注入中间件最方便,但得确保它生命周期和 server 一致,别在 handler 里反复 new。
常见坑是把 sem 放在 request scope 里,结果每个请求都新建一个,完全不起作用。
- 初始化一次,比如
var globalSem = semaphore.NewWeighted(50),然后在中间件闭包里引用 - 别在中间件函数体里做
new或init,否则每次路由匹配都新建实例 - 如果服务有多个 endpoint 需要不同并发上限,就定义多个
*semaphore.Weighted变量,按路径分发,别试图 runtime 修改权重
信号量本身不记录当前使用数,调试时想看实时占用量,得自己用 atomic.Int64 配合 Acquire/Release 手动统计 —— 这点文档没强调,但线上排障时很关键。










