go 的 semaphore.weighted 支持非1权重的许可申请,如 acquire(ctx, 3) 占3单位,但并非动态拆分资源粒度;它要求 acquire 与 release 的 weight 严格匹配,不校验非法 release,且仅在 context 取消时返回错误。

加权信号量在 Go 里没有内置实现
Go 标准库的 sync.Mutex 和 semaphore.Weighted(来自 golang.org/x/sync/semaphore)只支持单位权重——每次 Acquire 消耗 1 个许可。如果你需要“一个请求占 3 个槽、另一个占 1 个槽”,标准 semaphore.Weighted 本身就能做,但它不是“加权”在语义上容易误解的那个意思:它叫 Weighted,但实际就是支持非 1 的 weight 参数的信号量。
常见错误现象:semaphore.NewWeighted(10) 创建后调用 Acquire(ctx, 5) 成功,但后续再 Acquire(ctx, 6) 就阻塞——这不是 bug,是设计如此;很多人误以为“加权”意味着能动态拆分资源粒度,其实它只是允许单次申请多单位许可。
- 使用场景:限流 API 调用(如大文件上传占 3 单位,小文本请求占 1 单位)
-
Acquire的weight参数必须 ≥ 1,传 0 会 panic,负数直接 panic - 性能影响极小,底层是
sync.Pool+ CAS,无锁路径占主导 - 兼容性注意:Go 1.19+ 才默认支持
golang.org/x/sync/semaphore,旧版本需手动go get
正确初始化和 Acquire/Release 配对
最常踩的坑是 Acquire 成功后没确保 Release,尤其在 error 分支或 defer 中写错顺序。它不像 sync.Mutex 那样有对称的 Lock/Unlock 直觉。
示例片段:
立即学习“go语言免费学习笔记(深入)”;
sem := semaphore.NewWeighted(10)
err := sem.Acquire(ctx, 3)
if err != nil {
return err
}
defer sem.Release(3) // 必须和 Acquire 的 weight 一致!
// ... work
-
Release(n)的n必须等于之前Acquire的weight,否则计数错乱(不会 panic,但后续逻辑全崩) - 不要在多个 goroutine 里对同一
sem实例混用不同weight值做Release,没校验,纯靠人盯 - 如果函数可能多次
Acquire,要自己记账,sem不维护 per-goroutine 状态 - context 取消时
Acquire返回ctx.Err(),此时没占用许可,不用Release
与 channel 实现的信号量对比:别为了“看起来更 Go”而放弃 Weighted
有人倾向用带缓冲 channel 模拟信号量(make(chan struct{}, 10)),但这种方案天然不支持加权——你没法让一个 消费 3 个 slot。
- channel 方案:代码短,但无法表达非 1 权重,且
len(ch)是当前空闲数,不是已用数,易误读 -
semaphore.Weighted:API 明确区分TryAcquire/Acquire/Release,支持 context,可精确控权 - 性能上,channel 在高竞争下有调度开销,
Weighted是纯原子操作,实测吞吐高 2–3 倍(100+ goroutines 竞争时) - 别碰自研“加权 channel 包装器”,边界 case(如 panic 后 recovery、嵌套 acquire)极难覆盖全
超时、取消和 TryAcquire 的实用选择
真实服务中,你往往不想无限等一个权重为 5 的许可。这时候不能只依赖 Acquire(ctx, w),得组合用 TryAcquire 或控制 ctx。
-
TryAcquire(w)立即返回 bool,适合“尽力而为”场景(如缓存预热,没资源就跳过) - 想要超时:用
context.WithTimeout,例如ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond) - 千万别用
time.AfterFunc+ 单独 goroutine 去Release,会破坏配对逻辑 - 注意:
Acquire返回 error 仅两种:ctx.Canceled或ctx.DeadlineExceeded,不会返回 “资源不足” 错误——那是静默阻塞,直到有足够许可
复杂点在于:权重值本身如果是运行时计算出来的(比如按请求体大小线性换算),就得在 Acquire 前完成计算并校验是否 ≤ 总容量,否则可能因超限 panic。这个校验没人替你做。










