go语言中用带缓冲的chan struct{}实现信号量最直接:容量即最大并发数,如make(chan struct{}, 5)限制最多5个goroutine同时运行;它仅控制启动许可,不传参或收结果,任务需另起goroutine执行,发送/接收操作天然线程安全且阻塞式限流。

用 chan struct{} 实现信号量最直接
Go 没有内置信号量类型,但用带缓冲的 chan struct{} 是标准、轻量且符合 Go 思维的做法。它本质是计数器 + 阻塞:容量即最大并发数,比如 make(chan struct{}, 5) 表示最多 5 个 goroutine 同时运行。
常见错误是把它当任务队列用——它只管“能不能启动”,不传参、不收结果。任务逻辑仍需另起 goroutine 执行。
- 初始化后直接发送/接收即可,无需额外锁;
chan struct{}本身线程安全 - 别用
len(sem)判断剩余容量,它只是瞬时快照,不可靠 - 不要暴露底层 channel,封装成结构体更可控(如提供
Acquire()/Release()方法)
Acquire 和 Release 必须成对出现在 goroutine 内部
每个任务开始前必须先 sem ,否则阻塞;结束后必须 <code> 归还配额。漏掉释放会导致并发槽位永久卡死——这是最常发生的 panic 或提前 return 场景下的坑。
正确姿势是:在 goroutine 开头就注册 defer func() { ,确保无论正常返回、panic 还是 context 取消,都能释放。
- 不要在 goroutine 外部释放,否则会提前腾出槽位,破坏限流语义
- 不要在
select中把和 <code> 放同一层级,容易误触发释放 - 如果任务支持超时,
select的time.After分支里也要补上
必须搭配 sync.WaitGroup 才能知道任务真正结束
sem 只控制“启动节奏”,不跟踪“执行状态”。只靠它无法判断所有任务是否完成,主协程可能提前退出,留下 goroutine 泄漏。
典型组合是:wg.Add(n) 在启动前预设总数,每个 goroutine 内部 defer wg.Done(),主协程调用 wg.Wait() 阻塞等待全部退出。
- 别在获取
sem前调用wg.Add(1),否则可能因并发竞争导致多次 Add - 别在 goroutine 外部调用
wg.Done(),必须在 goroutine 内部、任务逻辑完成后 - 如果任务有返回值或错误,用额外的
chan收集,不要混进信号量逻辑里
context 取消时,sem 释放不能被跳过
当任务支持取消(如 HTTP 请求、数据库查询),不能只依赖 sem 的阻塞机制。如果 ctx.Done() 触发,goroutine 应尽快退出,但仍要确保 sem 被释放——否则槽位永久泄漏。
稳妥做法是:先获取 sem,再进入 select 监听 ctx;所有可能的 return 路径(包括 default、case )都由开头注册的 <code>defer 保证释放。
最容易忽略的是:在 select 里监听 ctx.Done() 后直接 return,忘了 defer 已注册但还没来得及执行——其实不会,因为 defer 总在函数返回前执行。真正危险的是没写 defer,或者写了但位置不对(比如放在 select 后面)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











