用带缓冲的chan struct{}实现信号量可控制goroutine并发数,缓冲大小即最大并发数,如make(chan struct{}, 10)限制最多10个goroutine同时运行,任务前需先获取信号量。

用 semaphore 控制 goroutine 并发数最直接
Go 没有内置的“并发数限制调度器”,但用带缓冲的 chan struct{} 实现信号量(semaphore)是标准做法。它本质是计数器 + 阻塞,比自己写锁+计数更简洁、更符合 Go 的 channel 思维。
常见错误是把缓冲通道当成任务队列——它只管“能不能启动”,不负责传参或结果。任务本身仍需另起 goroutine 执行。
- 缓冲大小即最大并发数,比如
make(chan struct{}, 10)表示最多 10 个 goroutine 同时运行 - 每个任务开始前必须先
sem ,否则阻塞;结束后必须 <code> 归还配额 - 别漏掉
—— panic 或提前 return 时容易忘记,建议用 <code>defer包裹
sync.WaitGroup 和 semaphore 必须配合使用
只靠 semaphore 无法知道所有任务是否完成:它只控制“启动节奏”,不跟踪“执行状态”。所以必须搭配 sync.WaitGroup 来等待全部结束。
典型组合模式是:外层用 WaitGroup.Add(n) 预设总数,每个 goroutine 内部 defer wg.Done(),主协程调用 wg.Wait() 阻塞直到全部退出。
- 不要在获取 semaphore 前调用
wg.Add(1),否则可能因并发竞争导致wg.Add被多次执行 - 也不要在 goroutine 外部释放 semaphore,必须在 goroutine 内部、任务完成后释放,否则会提前腾出并发槽位
- 如果任务有返回值或错误,建议用额外 channel 收集,不要塞进 semaphore 逻辑里
注意 context.Context 取消对 semaphore 的影响
当任务支持取消(如 HTTP 请求、数据库查询),不能只依赖 semaphore 的阻塞机制。如果 context 已取消,goroutine 应该尽快退出,但仍要确保 semaphore 被释放——否则并发槽位永久泄漏。
典型陷阱是:select 等待 context Done 后直接 return,忘了释放 。
- 释放 semaphore 的代码必须放在
defer中,且 defer 要在 goroutine 开头就注册 - 避免在
select中把和 <code> 放在同一层级,否则可能误触发释放 - 更稳妥的做法是:先获取 semaphore,再进入 select 监听 ctx,任务逻辑中任何 return 前都由 defer 保证释放
实际例子:并发请求 URL 列表
这是最常见场景,也是最容易暴露问题的地方。下面这段代码能安全控制并发数、正确等待、响应取消:
func fetchURLs(urls []string, maxConcurrent int) error {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
<pre class="brush:php;toolbar:false;">for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
sem <p>}</p>真正难处理的不是并发控制本身,而是边界情况:context 取消时 goroutine 是否已卡在 sem 上?HTTP 连接是否超时?Body 是否被读完?这些和 semaphore 无关,但它们共同决定了你能不能真正“限制住”资源消耗。











