
Go 里用 semaphore 控制并发请直接上 golang.org/x/sync/semaphore
标准库没有信号量,golang.org/x/sync/semaphore 是官方维护的、线程安全的信号量实现,专为流量控制设计。别自己用 chan struct{} 模拟,容易漏掉超时、取消、公平性等关键逻辑。
常见错误是把 semaphore.Weighted 当成“资源池”来反复 Acquire/Release 而不检查返回值——它可能返回 context.Canceled 或 context.DeadlineExceeded,忽略会导致 goroutine 永久阻塞或资源泄漏。
-
semaphore.NewWeighted(n)创建容量为n的信号量,n是最大并发数(整数),不是“令牌数”概念上的抽象值 - 每次
Acquire(ctx, 1)尝试获取 1 单位权重,成功才继续;失败必须显式处理错误,不能假设一定成功 - 务必配对调用
Release(1),且只能释放之前成功Acquire的数量,多放或少放都会破坏状态
Acquire 超时和取消必须由调用方控制
信号量本身不管理超时,ctx 是唯一出口。如果你传入 context.Background(),那 Acquire 会一直等下去,直到有空位——这在服务端场景极易引发级联雪崩。
典型场景:HTTP handler 中限制下游 API 并发调用,每个请求应自带 deadline:
立即学习“go语言免费学习笔记(深入)”;
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
if err := sem.Acquire(ctx, 1); err != nil {
http.Error(w, "too busy", http.StatusServiceUnavailable)
return
}
defer sem.Release(1)
- 永远不要在
Acquire前创建无取消的 long-lived context -
Acquire返回非nilerror 时,Release不可调用——没拿到资源,就不存在“释放”一说 - 如果需要支持中断重试,应在外层用
context.WithCancel,而不是依赖信号量内部机制
Weighted 支持非单位权重,但多数流量控制场景用不到
绝大多数限流需求是“最多 N 个并发”,即固定权重 1。启用非 1 权重(比如 Acquire(ctx, 3))会引入额外复杂度:资源分配不再均匀,统计难度上升,调试时难以对应到真实请求粒度。
除非你明确需要“大请求占 3 份、小请求占 1 份”这类混合调度策略,否则坚持用 Acquire(ctx, 1) + Release(1)。
- 权重总和不能超过初始化容量,否则
Acquire立即返回ErrNoResources - 不同权重混用时,等待队列不保证 FIFO,底层用
heap.Interface实现优先级调度,实际顺序取决于唤醒时机 - 监控信号量使用率时,
sem.CurrentCount()返回的是“已分配权重总和”,不是“持有者数量”,注意指标解读
别在 HTTP handler 里全局复用一个 semaphore.Weighted 实例就以为万事大吉
单实例没问题,但容易忽略两点:一是未区分业务维度(比如支付和查询共用同一信号量,一个慢接口拖垮全部),二是未动态调整容量(高峰期硬编码的 10 可能成为瓶颈)。
更稳妥的做法是按业务边界拆分信号量,或结合配置中心动态更新容量:
var (
paymentSem = semaphore.NewWeighted(5) // 独立控制支付链路
querySem = semaphore.NewWeighted(20) // 查询可更宽松
)
- 不要把信号量当成“万能熔断器”——它不感知下游响应时间、错误率,只管并发数
- 如果需要自动扩缩容,得在外层加监控+定时器,调用
semaphore的私有字段不可行,目前没提供运行时修改容量的公开方法 - 测试时记得用
time.Now().Add(-time.Hour)构造已过期的context,验证错误路径是否被正确覆盖










