用 make(chan struct{}, n) 创建信号量通道时,缓冲大小n即最大并发数,仅作占位不传数据,内存开销极小;需共享同一实例,不可设为0。

用 make(chan struct{}, N) 创建信号量通道
最直接的方式就是初始化一个带缓冲的空结构体通道:sem := make(chan struct{}, 10)。缓冲大小 10 就是最大并发数,它不传数据、只占位,内存开销几乎为零。
常见错误是误以为这个 channel 是任务队列——它只管“能不能启动”,不负责传递参数或返回结果。任务逻辑仍需另起 goroutine 执行。
注意别在循环里重复声明 sem,所有 goroutine 必须共享同一个实例;也别设成 0(无缓冲),那会变成同步阻塞,失去并发意义。
sem 和 <code> 必须成对出现
每个任务开始前必须先获取许可:sem ,否则会阻塞直到有空槽;结束后必须归还:<code>。漏掉后者会导致槽位永久泄漏,goroutine 数越跑越少,最终卡死。
容易踩的坑:
- panic 或提前
return时忘记释放,建议统一用defer func() { - 在 goroutine 外部释放,比如主协程里
,这会提前腾出槽位,破坏并发控制 - 把
sem 放在 <code>wg.Add(1)之后,可能因竞争导致wg.Add被多次执行
为什么不能只靠 sync.WaitGroup 或 runtime.GOMAXPROCS
sync.WaitGroup 只能等结束,无法在启动前判断是否该放行;runtime.GOMAXPROCS 控制的是 OS 线程(P)数量,不是 goroutine 并发上限——设成 1 不会让 1000 个 goroutine 串行,它们只是挤在单个 P 上频繁切换,反而加重调度压力。
真正需要的是准入控制:在 go f() 之前就决定“此刻能不能起”。信号量正是干这事的。
context 取消时仍要确保 被执行
如果任务支持取消(比如带 context.Context 的 HTTP 请求),goroutine 在收到 cancel 后应尽快退出,但退出路径上仍要保证 执行,否则槽位永远卡住。
典型写法是把释放逻辑包进 defer,并在任务主体中检查 ctx.Err() 提前 return:
go func() {
defer wg.Done()
sem <p>复杂点在于:所有退出路径(包括 panic recover、error return、context cancel)都得覆盖到释放逻辑,这是信号量使用的刚性要求,不是可选项。</p>golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











