大多数场景应选golang.org/x/sync/semaphore,因其支持context取消和超时,acquire(ctx,1)失败返回context.canceled或deadlineexceeded,避免goroutine永久挂起;仅当只需固定并发数、无超时且不响应取消时,才用更轻量透明的chan struct{}。

用 golang.org/x/sync/semaphore 还是 chan struct{}?
大多数场景该选 golang.org/x/sync/semaphore,除非你明确只需要固定并发数、无超时、不响应取消——这时 chan struct{} 更轻量、无依赖、行为透明。
-
semaphore.Weighted支持context取消和超时,Acquire(ctx, 1)失败会返回context.Canceled或context.DeadlineExceeded,避免 goroutine 永久挂起 - 手写
chan struct{}版本不会返回 error,sem 会一直阻塞,压测或下游卡顿时极易引发雪崩 - 如果你需要非单位权重(比如大任务占 2 个许可)、或要集成进带 context 的中间件链路,必须用官方包
- 初始化开销几乎可忽略:两者底层都基于 channel,
x/sync/semaphore只多一层原子计数器封装
semaphore.Weighted.Acquire 必须检查 error
不检查 Acquire 返回的 error 是最常见泄漏源头。它不是“大概率成功”,而是“失败即不可继续”。
- 错误类型只可能是
context.Canceled或context.DeadlineExceeded,别再嵌套判断err != nil后又查errors.Is(err, context.Canceled) -
Acquire失败时,Release绝对不能调用——没拿到许可,释放就是未定义行为 - 典型误用:
if err := sem.Acquire(ctx, 1); err != nil { log.Println("reject"); return },但忘了在 return 前释放其他已持有的资源(比如 DB 连接) - 正确姿势:把
Acquire放在业务逻辑最前,失败直接返回;成功后立刻defer sem.Release(1)
semaphore.Weighted.Release 的配对陷阱
释放数量必须与 Acquire 申请数量严格一致,且只能由同一 goroutine 调用。错配会导致计数器永久失准。
-
Acquire(ctx, 3)后写三次Release(1)不 panic,但若中间 panic 或提前 return,极易漏掉某次释放 - 推荐写法:
defer sem.Release(3),确保一次归还全部 - 跨 goroutine 调用
Release是危险操作:虽然线程安全,但语义上破坏了“租约归属”,后续Acquire可能拿到已被逻辑释放的许可 -
sem.CurrentCount()返回的是“已分配权重总和”,不是“当前持有者数量”——如果你用不同权重混用,监控值会和直觉不符
HTTP handler 中嵌入信号量的常见坑
信号量实例必须是全局或 server 级生命周期,不能按请求 new,也不能藏在闭包里被重复初始化。
- 错误做法:在 handler 函数内
sem := semaphore.NewWeighted(5)—— 每次请求都新建一个,完全不起限流作用 - 正确做法:定义为包级变量或注入到 handler 结构体中,随 server 启动初始化一次
- 别把信号量和
sync.WaitGroup混用:前者控“同时跑几个”,后者数“跑完了几个”,目标完全不同 - 如果用
errgroup.Group,优先调它的SetLimit方法——它内部封装了semaphore,并自动处理 panic 和 context 传播,比裸用更稳
真正难的不是选哪个 API,而是保证每次 Acquire 都有对应的 Release,且中间不被 panic、return 或 context 取消绕过。哪怕用了官方包,漏掉一次 defer,整个信号量就失效了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











