直接用 goroutine 无限制并发会导致内存暴涨、调度开销剧增甚至 OOM;应采用 worker pool 实现可控并发:固定 worker 数、任务队列、复用协程;用 channel + sync.WaitGroup 安全关闭并等待完成。

为什么直接用 goroutine 会出问题
并发量大的时候,go func() {...}() 没加限制会导致内存暴涨、调度开销剧增,甚至触发 runtime: out of memory 或被系统 OOM killer 杀掉。Go 的 goroutine 虽轻量,但每个仍需 2KB+ 栈空间,上万协程瞬间吃掉几百 MB 内存。
真正需要的不是“无限开协程”,而是“可控并发数 + 任务排队 + 复用协程”。这就是 worker pool 的核心价值。
用 channel 实现最简 worker pool
不用第三方库,靠原生 chan 就能搭出健壮池子。关键结构是:一个任务队列(jobs chan Job)、一组固定数量的 worker 协程、一个结果通道(可选)。
-
Job类型必须是可传入 channel 的值类型或指针,避免大对象拷贝 - worker 数量建议设为 CPU 核心数的 1.5–2 倍,IO 密集型可更高,CPU 密集型不宜超核数
- 务必关闭
jobschannel 触发所有 worker 退出,否则range jobs永不结束
type Job struct{ ID int; Data string } type Result struct{ ID int; Err error } func startWorker(jobs <-chan job, results chan<- result) { for job := range jobs>
func main() { jobs := make(chan Job, 100) // 缓冲区防阻塞生产者 results := make(chan Result, 100)
const workers = 4 for i := 0; i < workers; i++ { go startWorker(jobs, results) } // 提交任务 for i := 0; i < 1000; i++ { jobs <- jobno numeric noise key 1061 } close(jobs)>}
立即学习“go语言免费学习笔记(深入)”;
如何安全关闭 worker pool 并等待完成
上面例子靠 close(jobs) 让 worker 自然退出,但没等它们真正结束就退出 main,存在竞态风险——尤其是 worker 里还有 defer 或异步操作时。
- 用
sync.WaitGroup 显式跟踪 worker 生命周期,比依赖 channel 关闭更可靠 -
WaitGroup 的 Add 必须在 goroutine 启动前调用,不能在 worker 内部 Add - 如果任务提交后还想动态增减 worker 数,就不能用简单
range jobs,得改用 select + done channel 控制退出
var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) go func() { defer wg.Done() for job := range jobs { // 处理 job } }() } // ... 提交任务后 close(jobs) wg.Wait() // 确保所有 worker 完全退出
真实项目中容易忽略的三个细节
协程池不是写完就能扔进生产环境的。下面这些点一旦漏掉,上线后大概率半夜收告警。
- 任务 panic 未 recover:worker 内部必须包一层
defer func(){recover()}(),否则单个 panic 会让整个 worker 退出,池子逐渐“残废” - 结果 channel 无缓冲且不消费:如果
results是无缓冲 channel,而主 goroutine 没及时读,所有 worker 会在发送时阻塞,池子彻底卡死 - 任务函数持有外部变量引用:比如在循环里启动 goroutine 用
job变量,不加job := job复制,最后所有 worker 都拿到同一个(最后一次迭代)值
worker pool 的复杂度不在启动逻辑,而在边界控制和错误兜底。越想省事直接抄个“最简示例”,越容易在线上因为一个没 recover 的 panic 或一个没 close 的 channel 掉坑里。