Go 1.21+ 使用 sync.Semaphore 是控制 goroutine 并发数最稳妥的方式,它基于带权重信号量实现无竞态、可取消的并发限制,避免手写计数器易导致的漏释放、panic 未恢复及取消响应缺失等问题。

用 semaphore 控制 goroutine 并发数最稳妥
Go 没有内置的“并发数限制”原语,但用 sync.Semaphore(Go 1.21+)是最直接、无竞态、可取消的方式。它本质是带权重的信号量,每次 Acquire 占一个许可,Release 归还,超限时自动阻塞。
常见错误是手写计数器 + sync.Mutex:容易漏 Unlock、panic 后没释放、或在 defer 里释放但没考虑上下文取消。
-
semaphore.NewWeighted(int64(n))初始化时传入最大并发数n,注意类型是int64 - 每个 goroutine 启动前调用
sem.Acquire(ctx, 1),失败(如 ctx 被 cancel)就跳过 - 务必在 goroutine 结束时调用
sem.Release(1),建议放在函数最外层 defer - 如果任务本身耗时长,且需响应取消,把同一个
ctx传给业务逻辑,别只用在Acquire
sem := semaphore.NewWeighted(3)
for _, task := range tasks {
if err := sem.Acquire(ctx, 1); err != nil {
continue // ctx cancelled 或超时
}
go func(t Task) {
defer sem.Release(1)
doWork(t)
}(task)
}旧版本 Go(chan struct{} 模拟信号量
本质是带缓冲的 channel,容量即并发上限。比 mutex 计数更安全,因为发送/接收天然配对,不会因 panic 漏释放。
坑在于:channel 容量固定,不能动态调整;若 goroutine panic 未 recover,chan <- struct{} 永远卡住,整个程序可能假死。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 声明:
sem := make(chan struct{}, 3),缓冲大小就是最大并发数 - 获取许可:
sem <- struct{}{}(阻塞直到有空位) - 释放许可:
<- sem(从 channel 取出一个零值) - 必须确保每个
<- sem都被执行,推荐用 defer 封装
sem := make(chan struct{}, 3)
for _, task := range tasks {
sem <- struct{}{} // 获取许可
go func(t Task) {
defer func() { <- sem }() // 释放许可
doWork(t)
}(task)
}别用 runtime.GOMAXPROCS 控制并发数
runtime.GOMAXPROCS 控制的是 OS 线程(M)数量,不是 goroutine 并发数。它影响调度器并行度,和你的业务逻辑并发无关。
典型误用:设成 2 就以为最多跑 2 个 goroutine —— 实际上成千上万个 goroutine 仍会排队运行,只是底层线程只有 2 个在干活。该卡还是卡,该超时还是超时。
-
GOMAXPROCS默认是 CPU 核心数,改小可能降低 I/O 密集型程序吞吐 - 它不解决资源竞争(如数据库连接池打满)、API 限流、或下游服务压垮等问题
- 真要控并发,请用信号量、worker pool 或第三方库如
golang.org/x/sync/errgroup+ context
goroutine 泄漏常被忽略的点
并发控制代码写对了,但 goroutine 还是越积越多,大概率是泄漏。核心原因是:没有等 goroutine 结束,或 channel 关闭后还在往里发数据。
尤其在用 chan struct{} 方案时,如果 sender 先退出、receiver 还在读,channel 永远不关闭,goroutine 就卡在 <- sem 上。
- 所有启动的 goroutine,必须有明确的退出路径(比如通过 channel 关闭、context Done、或返回 error)
- 避免在循环里无条件起 goroutine,尤其是带 channel 操作的,要检查 channel 是否已 close
- 用
pprof查看/debug/pprof/goroutine?debug=2,找长期阻塞在 send/recv 的协程 - 测试时加
time.Sleep看是否 goroutine 数稳定,比只看逻辑更可靠
实际用起来,sync.Semaphore 是首选,但得记得 release 必须和 acquire 成对;旧版用 channel 模拟也够用,关键是别让 defer 失效。最难防的不是写错,而是忘了 goroutine 自己怎么停下来。

















