应使用 golang.org/x/sync/semaphore 而非 chan struct{}:前者是官方推荐、支持上下文取消和超时,后者无法响应 ctx.done()、无超时、tryacquire 需手动实现且易漏释放。

别用 chan struct{} 手写“伪信号量”,直接上 golang.org/x/sync/semaphore —— 它是 Go 官方维护的、语义正确、支持上下文取消的唯一推荐实现。
为什么不能用 make(chan struct{}, N) 模拟信号量
它看起来简洁,但本质不是信号量:不响应 ctx.Done(),没有超时机制,TryAcquire 需要自己套 select + default,且在 panic 或提前 return 时极易漏掉 ,导致 goroutine 永久阻塞。更隐蔽的问题是——它无法区分“等待被取消”和“资源暂时不可用”,错误处理分支容易写成空逻辑或 panic。
常见翻车场景:
- HTTP handler 中用
select { case ch ,但没处理 <code>ctx.Done()分支,客户端断开后 goroutine 仍卡在 channel 发送上 - 封装成方法后,
Release()被无条件defer,而Acquire()根本没成功,结果永久阻塞 - 想支持权重(比如大任务占 2 单位),只能硬改 channel 类型或加额外计数器,破坏原子性
semaphore.Weighted.Acquire 和 TryAcquire 怎么选
Acquire 是阻塞式租约获取,必须传入有效 context.Context;TryAcquire 是非阻塞试探,立刻返回 bool,不关心上下文。
使用原则:
- 服务端限流(如 HTTP handler)一律用
Acquire(ctx, 1),配合context.WithTimeout或context.WithCancel,失败就快速返回 503 - 后台批处理调度可用
TryAcquire(1),失败则退避重试或丢弃,避免阻塞主流程 - 绝不要把
TryAcquire当成 “轻量Acquire” 而忽略返回值判断——返回false时后续逻辑必须跳过临界区 -
Acquire的第二个参数是int64,支持一次申请多个单位;TryAcquire只接受int64,不接受context
Release 必须和 Acquire 严格配对,且只在成功后调用
semaphore.Weighted 不维护“谁持有”的元信息,只维护一个原子计数器。释放操作不是“归还资源”,而是“履行租约”。一旦错配,状态立即错乱。
关键约束:
-
Acquire(ctx, 3)成功后,必须调用Release(3);拆成三次Release(1)虽不 panic,但会破坏公平性与监控指标(CurrentCount()返回的是已分配总权重,不是持有者数量) -
Acquire返回非nilerror 时,绝对不可调用Release—— 此时你根本没拿到许可,释放无意义,还可能触发内部 panic - 必须由同一 goroutine 调用
Acquire和对应Release;跨 goroutine 释放会导致计数器溢出或死锁 - 推荐写法:
if err := sem.Acquire(ctx, 1); err != nil { return err }; defer sem.Release(1)
初始化和权重设置的几个硬限制
semaphore.NewWeighted(n) 的 n 是最大可用权重总和,类型为 int64,必须 > 0。传 0 或负数会 panic。
权重不是“抽象令牌”,而是真实资源消耗单位:
- 绝大多数场景坚持用
Acquire(ctx, 1),避免引入非均匀调度复杂度 - 若启用非 1 权重(如
Acquire(ctx, 2)),需确保所有请求的权重总和 ≤n,否则Acquire立即返回ErrNoResources - 混合权重下,等待队列不保证 FIFO;底层用
heap.Interface实现唤醒优先级,实际顺序取决于 runtime 调度时机 - 监控时注意:
sem.CurrentCount()返回已分配权重总和,不是当前活跃 goroutine 数量 —— 一个 goroutine 占 3 单位,另一个占 1 单位,CurrentCount()就是 4
最常被忽略的一点:信号量本身不管理生命周期,Release 的安全性完全依赖调用方是否严格遵循“成功才释放”。哪怕只漏掉一次,后续所有 Acquire 都可能永久阻塞 —— 这种错误在线上极难复现和定位。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











