用 semaphore 控制 goroutine 并发数最稳妥:以带缓冲 channel 作信号量,容量设为最大并发数,每次启动前发送令牌,完成后接收释放。

用 semaphore 控制 goroutine 并发数最稳妥
Go 没有内置的“并发数限制”原语,但用 semaphore(信号量)模式实现最直接、无副作用。核心是用带缓冲的 channel 当计数器:容量即最大并发数,每次启动 goroutine 前 (阻塞直到有空位),结束后 <code>sem 归还额度。
常见错误是把 sem 声明成 chan int 却忘了初始化缓冲区,或漏掉归还操作导致后续任务永远卡住。
- 初始化:
sem := make(chan struct{}, 5)表示最多 5 个并发 - 启动前必须先获取:
sem (注意不是 <code>select或非阻塞写) - 务必在 goroutine 结束时归还:
defer func() { sem ,否则会泄漏 - 不要用
time.Sleep模拟耗时任务来测试——它不释放 OS 线程,可能掩盖调度问题
用 errgroup.Group + WithContext 简化带限流的批量任务
如果任务本身要返回 error、支持取消、且需统一等待完成,errgroup.Group 比裸写 channel 更安全。它的 SetLimit 方法本质就是封装了 semaphore,但自动处理 panic 捕获和上下文传播。
注意:Go 1.21+ 才支持 SetLimit;旧版本得自己 wrap errgroup.Group 或换方案。
- 启用限流:
g.SetLimit(8),之后所有g.Go调用都会受控 - 必须搭配
context.WithTimeout使用,否则超时任务无法中断正在运行的 goroutine -
g.Wait()会返回第一个非 nil error,但其他 goroutine 仍会继续执行——若需强一致性中断,得靠 context cancel 通知内部逻辑退出 - 别在
g.Go的函数里直接用log.Fatal,它会终止整个进程,绕过 errgroup 的错误收集
避免用 runtime.GOMAXPROCS 误当并发控制
runtime.GOMAXPROCS 控制的是**可同时执行的 OS 线程数**,不是 goroutine 并发数。设成 1 不代表任务串行,只是同一时刻最多一个 P 在跑,大量 goroutine 仍会排队调度,内存和上下文切换开销照旧。
典型误用场景:想限制 HTTP client 并发请求,却去调 GOMAXPROCS(2),结果发现连接池、DNS 解析、TLS 握手全没被限制,QPS 还是打满下游。
- 真正需要限流的地方(如 API 调用、DB 查询、文件读写)必须在业务逻辑层做,不能依赖调度器参数
-
GOMAXPROCS一般保持默认(等于 CPU 核心数)即可,除非明确遇到 P 频繁抢占或 NUMA 问题 - 压测时若发现 goroutine 数暴涨但 CPU 利用率低,大概率是 I/O 阻塞未处理好,而不是 GOMAXPROCS 设小了
HTTP 客户端级并发限制容易被忽略的点
即使你的业务代码用 semaphore 控制了 goroutine 数,http.Client 默认的连接池仍可能突破限制:每个 host 默认 MaxIdleConnsPerHost = 100,短连接复用会导致瞬间涌出大量 TCP 连接。
必须显式收紧 Transport 配置,否则限流形同虚设。
- 设置全局连接上限:
http.DefaultTransport.(*http.Transport).MaxIdleConns = 10 - 按 host 限流更精准:
MaxIdleConnsPerHost = 5(建议 ≤ 你的 semaphore 容量) - 启用 keep-alive 但设合理 idle timeout:
IdleConnTimeout = 30 * time.Second,防连接堆积 - 若用自定义
http.Client,记得传入配置好的Transport,别只改DefaultTransport
实际部署时,goroutine 限流值不是拍脑袋定的。它得结合下游服务的吞吐能力、单次任务平均耗时、以及你容忍的最大延迟毛刺来反推。比如下游稳定扛 200 QPS、任务平均耗时 200ms,那理论并发上限就是 200 * 0.2 = 40,再打七折留余量,设 28 比设 50 更靠谱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











