go中可用带缓冲channel实现信号量:make(chan struct{}, n),acquire()发送、release()接收,零内存开销且语义清晰。

Go 里没有内置的 Semaphore 类型,得自己造
Go 标准库确实没提供 Semaphore,但用 sync.Mutex + sync.Cond 或更简单的 chan struct{} 就能稳稳实现。别被“信号量”这个词唬住——它本质就是个带计数的资源池,控制同时最多几个 goroutine 能进临界区。
最常用、最轻量的做法是用带缓冲的 channel:make(chan struct{}, N)。往里发一个 struct{} 占坑,取一个出来放行,零内存开销,语义清晰。
- 缓冲大小
N就是最大并发数,设错会导致死锁或放行过多 -
struct{}是零尺寸类型,不会额外分配内存,比用bool或int更干净 - 别用
close()清空 channel——关了就再也写不进去了,会 panic
用 chan struct{} 实现阻塞式获取与释放
核心就两步:获取时向 channel 发送(阻塞直到有空位),释放时从 channel 接收(腾出一个位置)。注意必须成对调用,且释放操作不能漏,否则资源永远卡住。
type Semaphore struct {
ch chan struct{}
}
<p>func NewSemaphore(n int) *Semaphore {
return &Semaphore{ch: make(chan struct{}, n)}
}</p><p>func (s *Semaphore) Acquire() {
s.ch <- struct{}{}
}</p><p>func (s *Semaphore) Release() {
<-s.ch
}-
Acquire()是阻塞的,适合需要严格控并发的场景(比如限制 HTTP client 并发请求数) - 如果想非阻塞尝试获取,用
select { case s.ch - 千万别在 defer 里无条件
Release()—— 如果Acquire()永远没成功,Release()会永久阻塞
和 sync.WaitGroup / sync.Pool 别混用
WaitGroup 数的是 goroutine 完成数,Pool 缓存临时对象,它们都不管“同时运行几个”。硬凑着用会逻辑错乱。比如有人用 WaitGroup.Add(1) 在入口、Done() 在出口模拟限流,结果发现并发数完全失控——因为 WaitGroup 不阻塞,只是计数。
立即学习“go语言免费学习笔记(深入)”;
- 要控“正在跑的数量”,只认信号量(channel 或
Cond) -
sync.Once是单次初始化,跟并发控制无关 - 用
context.WithTimeout包裹Acquire()可防无限等待,尤其在外部依赖可能卡住时
实际压测中容易崩的点:释放时机和 panic 后的兜底
真实服务里,goroutine 可能在 Acquire() 后、业务逻辑中间 panic,导致 Release() 永远不执行。缓冲 channel 慢慢被占满,后续所有请求全卡死。
- 必须用
defer配合 recover,或者用defer s.Release()+if err != nil { return }提前退出路径 - 更稳妥的做法是把 acquire/release 包进函数闭包,强制成对:
func (s *Semaphore) Execute(f func()) {
s.Acquire()
defer s.Release()
f()
}真正难的不是写出来,是确保每个路径都释放——尤其是错误分支、超时分支、提前 return 分支。漏一次,整个服务的并发能力就掉一块。










