go 无内置信号量,但可用带缓冲的 chan struct{} 模拟:初始化 sem := make(chan struct{}, maxconcurrency),取令牌用

Go 里没有内置的 semaphore,但可以用 chan struct{} 模拟
Go 标准库不提供传统意义上的信号量(如 POSIX semaphore),但用带缓冲的 channel 实现一个轻量、线程安全的并发限制器非常自然。核心思路是:把 channel 当作“令牌池”,每次执行前取一个 token,执行完放回一个。
常见错误是误用无缓冲 channel(make(chan struct{}))——它只能串行;或缓冲大小设错,导致实际并发数和预期不符。
- 初始化:
sem := make(chan struct{}, maxConcurrency),其中maxConcurrency就是你想限制的最大 goroutine 数 - 获取许可:
sem (阻塞直到有空位) - 释放许可:
(从 channel 取出一个 token,相当于归还) - 注意:必须确保每个
都有对应 <code>sem ,否则会死锁
封装成可复用的 Limit 函数,支持超时和 panic 安全
直接裸写 channel 操作容易漏掉释放逻辑,尤其遇到 panic 或提前 return。推荐封装为函数,用 defer 保证释放。
func Limit(max int, f func()) {
sem := make(chan struct{}, max)
sem <p>但这样每次调用都要新建 channel,开销大。更实用的是预建一个全局或局部 <code>sem</code>,然后在闭包中复用:</p>
- 超时控制:用
select+time.After避免无限等待sem - panic 安全:用
recover()+defer确保即使f()panic,token 也能归还 - 不要把
sem传给多个 goroutine 后随意 close —— close 后再写入会 panic
用 golang.org/x/sync/semaphore 替代手写(Go 1.21+ 推荐)
如果你用的是 Go 1.21 或更新版本,标准生态已提供官方维护的信号量实现:golang.org/x/sync/semaphore。它比手写 channel 更健壮,支持带上下文的 acquire、支持权重(acquire 多个 unit)、支持 cancel 和 timeout。
典型用法:
import "golang.org/x/sync/semaphore"
<p>sem := semaphore.NewWeighted(int64(max))
ctx := context.Background()
if err := sem.Acquire(ctx, 1); err != nil {
// 超时或被 cancel
return
}
defer sem.Release(1)</p><p>f()</p>
-
Acquire返回 error,需显式检查;Release不返回 error,但必须和Acquire的 weight 匹配 - 如果函数可能长时间阻塞,建议传入带 timeout 的
ctx,避免 goroutine 卡死在Acquire - 这个包不是标准库一部分,需要
go get golang.org/x/sync/semaphore
别混淆 semaphore 和 sync.WaitGroup 或 sync.Mutex
有人试图用 WaitGroup 控制并发数,结果发现它只管“等待结束”,不管“同时运行几个”;也有人用 Mutex 加锁后计数,但这会引入竞态和复杂性,且无法天然阻塞。
-
WaitGroup:适合等一堆 goroutine 全部完成,不控制并发度 -
Mutex+ 计数器:需要额外 lock/unlock + 条件变量,易出错,性能也不如 channel 或x/sync/semaphore - 真正要限制“最多 N 个同时执行”,就该用信号量语义 —— 获取许可 → 执行 → 归还许可
最常被忽略的一点:信号量控制的是“执行中”的 goroutine 数,不是“启动了但还没执行”的数量。如果任务本身启动快、执行慢,那限制才生效;如果启动慢(比如大量初始化),并发数可能远低于设定值。











