Go中无全局goroutine并发数限制,真正需控制的是同时活跃的业务任务数,推荐用带缓冲channel(如make(chan struct{}, N))实现轻量限流,而非调整GOMAXPROCS。

goroutine 本身没有全局最大并发数限制
Go 运行时不会限制你启动多少个 goroutine,go f() 调用几乎不占系统资源(初始栈仅 2KB),所以「设置最大并发数」不是调 runtime.GOMAXPROCS 或改某个开关就能生效的事——那是对调度器的误解。真正需要控制的是「同时活跃执行的业务逻辑数量」,比如同时处理多少个 HTTP 请求、多少个文件下载、多少个数据库查询。
用带缓冲的 channel 实现并发数限制
这是最轻量、最常用、也最符合 Go 风格的做法:把 channel 当作一个“令牌桶”,容量即最大并发数。每个任务启动前先从 channel 取一个 token,完成后归还。
常见错误是把 channel 声明在循环内,导致每次新建一个无缓冲 channel,完全失效;或者忘记归还 token,造成 goroutine 泄漏。
正确写法示例:
立即学习“go语言免费学习笔记(深入)”;
// 最大并发数设为 5
sem := make(chan struct{}, 5)
<p>for _, task := range tasks {
sem <- struct{}{} // 获取令牌(阻塞直到有空位)
go func(t Task) {
defer func() { <-sem }() // 一定要用 defer 归还,避免 panic 后漏还
process(t)
}(task)
}注意:struct{} 零内存占用,比用 int 更合适;defer 放在 goroutine 内部,确保每个 goroutine 自己负责归还。
用 sync.WaitGroup + 计数器容易出竞态
有人尝试用 sync.WaitGroup 加全局计数器(如 atomic.Int64)来控制并发,但极易引入竞态或死锁:比如判断计数未超限后、实际启动 goroutine 前,另一个 goroutine 已抢先占满额度。
除非你额外加锁(那就失去轻量优势),否则不推荐。channel 的发送操作天然具备原子性与排队语义,sem 这一行既是“检查是否可进”,也是“登记入场”,一步到位。
其他替代方案如 golang.org/x/sync/semaphore 库功能更全(支持带上下文的 acquire),但对简单限流来说,原生 channel 更直接、无依赖、易理解。
别混淆 GOMAXPROCS 和并发控制
runtime.GOMAXPROCS(n) 控制的是**可并行执行的操作系统线程数**(即 P 的数量),影响的是 CPU 密集型任务的并行度,和「同时跑多少个 goroutine」完全无关。哪怕 GOMAXPROCS(1),你照样可以起 10 万 goroutine 处理 I/O,它们会在单个 OS 线程上由 Go 调度器协作式调度。
如果你的程序卡顿、内存暴涨、pprof 显示 goroutine 数量异常高,问题不在 GOMAXPROCS,而在没做业务层并发节制——该用 channel 限流的地方用了无限 go。
真正难的不是写对那几行限流代码,而是识别出哪些 goroutine 是「必须并发」的,哪些是「可以排队」的;比如日志写入、监控上报这类非关键路径,往往值得主动降并发保主流程稳定。


















