直接用 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 {
// 模拟处理
if job.ID%100 == 0 {
results <- Result{ID: job.ID, Err: fmt.Errorf("failed on %d", job.ID)}
} else {
results <- Result{ID: job.ID, Err: nil}
}
}
}
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 <- Job{ID: i, Data: "payload"}
}
close(jobs) // ⚠️ 关键:通知 worker 任务结束
// 收集结果(注意:要等够 1000 个)
for i := 0; i < 1000; i++ {
res := <-results
if res.Err != nil {
log.Printf("job %d failed: %v", res.ID, res.Err)
}
}}
立即学习“go语言免费学习笔记(深入)”;
如何安全关闭 worker pool 并等待完成
上面例子靠 close(jobs) 让 worker 自然退出,但没等它们真正结束就退出 main,存在竞态风险——尤其是 worker 里还有 defer 或异步操作时。
- 用
sync.WaitGroup显式跟踪 worker 生命周期,比依赖 channel 关闭更可靠 -
WaitGroup的Add必须在 goroutine 启动前调用,不能在 worker 内部 Add - 如果任务提交后还想动态增减 worker 数,就不能用简单
range jobs,得改用select+donechannel 控制退出
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 掉坑里。










