推荐用 make(chan struct{}, n) 实现信号量:struct{} 零内存占用、语义清晰;获取许可需发送 struct{},释放许可需接收,严禁混用 chan int 或 chan bool。

用 chan struct{} 实现信号量,别直接用 chan int
Go 里没有内置 Semaphore 类型,但用带缓冲的 channel 就能准确模拟:容量即许可数,发送/接收操作天然线程安全。关键不是“能不能用”,而是“怎么用才不翻车”。
常见错误是用 chan int 或 chan bool 存具体值,结果误把信号量当成数据通道——它只该表示“有没有空位”,不该承载业务数据。
-
make(chan struct{}, n)是唯一推荐方式:struct{}零内存占用,语义清晰 - 获取许可:向 channel 发送一个
struct{}(sem ),若已满则阻塞 - 释放许可:从 channel 接收一次(
<-sem),唤醒等待的 goroutine - 注意:不要在 select 中无默认分支地尝试非阻塞获取,否则可能漏掉许可或 panic
为什么 sync.Mutex + 计数器不能替代 chan struct{}
有人试图用互斥锁保护一个整型计数器来模拟信号量,逻辑看似可行,但会破坏 Go 的调度语义和资源感知能力。
典型问题包括:goroutine 等待时无法被系统级中断(比如收到 SIGINT)、无法与 context.WithTimeout 配合、无法参与 Go 运行时的 goroutine 唤醒公平性调度。
立即学习“go语言免费学习笔记(深入)”;
- channel 阻塞是 runtime 层面的,可响应 cancel、timeout、deadline
- 用
sync.Mutex实现的“伪信号量”在超时场景下必须手动清理状态,极易出错 - 性能上,
chan struct{}在低争用时开销接近原子操作;而锁+计数器每次都要加锁,还多一次内存读写
sem 卡住?先检查是否漏了 <code><-sem 或 goroutine 泄漏
最常遇到的现象是某个 goroutine 永久阻塞在 sem ,程序看起来“卡死”。这不是 channel 本身的问题,而是许可没被正确归还。
原因往往藏在 defer、panic 恢复、或提前 return 的路径里——只要有一处没执行 <-sem,那个许可就永远丢失。
- 务必把
<-sem放在 defer 中,且 defer 要紧贴 acquire 后面(sem ) - 避免在 select 中混用多个 channel 时,只处理部分 case 就退出,导致未归还许可
- 用
runtime.NumGoroutine()辅助判断是否 goroutine 积压;用 pprof 查看阻塞在 channel send 的堆栈
并发数动态调整?别改 channel 容量,换用 sem := make(chan struct{}, newN) 重建
channel 的缓冲区大小在创建后不可变。试图通过关闭旧 channel、新建一个再迁移 goroutine 是危险的——没有原子切换机制,极易出现竞态或 goroutine 永久等待。
真正需要动态限流的场景(比如根据负载自动缩放),应在外层做控制,而不是动信号量本身。
- 把
chan struct{}封装成结构体,提供Acquire(ctx context.Context)和Release()方法 - 动态调整时,让新请求走新 channel,旧 channel 上的 goroutine 自然完成并退出(配合 context Done)
- 切忌用
close()来“清空”信号量 channel——关闭后 send 会 panic,recv 会立即返回零值,彻底破坏语义
实际用起来,最难的从来不是写对那两行 channel 操作,而是确保每个 acquire 都有对应 release,且都在正确的作用域和错误路径里。稍一松懈,泄漏就发生了。










