直接用 sync.Mutex 无法实现并发请求数限制,因其仅支持单 goroutine 进入,而信号量需控制最多 N 个;应使用容量为 N 的 chan struct{} 模拟信号量,send 消耗令牌、recv 归还令牌。

为什么直接用 sync.Mutex 不能做并发请求数限制
因为 sync.Mutex 只能保证「同一时间最多 1 个 goroutine 进入」,而信号量要的是「最多 N 个」。你得自己维护一个计数器并配合条件等待,但手动写容易漏掉 Unlock 或死锁——比如某个 goroutine panic 后没释放,后续全部卡住。
更稳妥的做法是用 semaphore 模式,本质是带计数的通道或原子变量 + 条件变量。Go 标准库没直接提供,但可以用 chan struct{} 快速实现,它天然支持阻塞与释放语义。
- 容量为
N的chan struct{}就是 N 个“令牌”,send操作消耗一个,recv归还一个 - 注意:必须用
make(chan struct{}, N),不能用make(chan struct{}, 0)(那是互斥锁) - 别在 defer 中无条件
sem ,如果 acquire 失败(比如超时),这个 defer 会 panic
用 chan struct{} 实现可超时的信号量函数
实际业务中常需要「等 5 秒,拿不到就放弃」,所以得支持上下文控制。下面这个 Acquire 函数返回 bool 表示是否成功获取,调用者负责在完成后调用 Release。
type Semaphore struct {
c chan struct{}
}
<p>func NewSemaphore(n int) *Semaphore {
return &Semaphore{c: make(chan struct{}, n)}
}</p><p>func (s *Semaphore) Acquire(ctx context.Context) bool {
select {
case s.c <- struct{}{}:
return true
case <-ctx.Done():
return false
}
}</p><p>func (s *Semaphore) Release() {
<-s.c
}-
Acquire是非阻塞尝试 + 超时等待,避免 goroutine 无限挂起 -
Release必须被调用,否则资源永久泄漏;建议用defer sem.Release(),但只在Acquire返回true后才 defer - 如果并发量极大(比如每秒上万请求),
chan的锁开销略高;此时可换用sync/atomic+sync.Cond,但复杂度上升,多数场景没必要
和 golang.org/x/sync/semaphore 的关键差异
官方扩展包 golang.org/x/sync/semaphore 提供了更健壮的实现,但它默认不支持「立即失败」(即 try-acquire),且 Acquire 接口要求传入 *runtime.Park-style 的 context.Context,对简单限流有点重。
立即学习“go语言免费学习笔记(深入)”;
- 标准库
chan方案:轻量、易懂、适合中小流量(QPS -
x/sync/semaphore:支持权重(weighted)、可取消、底层用sync.Pool复用 waiter,适合高频或需精确控制的场景 - 两者都不处理「持有者崩溃」问题——Go 没有 finalizer 机制,panic 后无法自动 release,必须靠业务层保障
在 HTTP handler 中安全使用信号量的典型模式
常见错误是把 Semaphore 实例定义在 handler 内部,导致每次请求新建一个,完全失效。必须是全局或依赖注入的单例实例。
var sem = NewSemaphore(10)
<p>func handler(w http.ResponseWriter, r <em>http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3</em>time.Second)
defer cancel()</p><pre class="brush:php;toolbar:false;">if !sem.Acquire(ctx) {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
defer sem.Release()
// 执行真实业务逻辑,比如调第三方 API
resp, err := callExternalAPI(r.Context())
// ...}
- 超时时间设得太短(如 100ms)会导致大量请求被拒,太长(如 30s)会让排队雪崩;建议基于下游 P99 延迟 × 2~3 倍设置
- 不要在
Release前做可能 panic 的操作(如 JSON 解析),否则信号量永远卡死;必要时加 recover - 如果服务同时处理多种请求类型(如读/写),应为不同类型分配独立信号量,避免互相阻塞
真正麻烦的从来不是写对那几行代码,而是确认「谁该释放」「什么时候释放」「释放失败了怎么办」——这些没法靠工具自动兜底,得靠设计时就想清楚责任边界。


















