goroutine 很轻量,但“轻量”不等于“无限开”。一旦任务量上来,不受控地起 goroutine 很容易把下游数据库、CPU 或第三方 API 一起打爆。

这时候,worker pool 是最常用的限流模型之一。

核心思想

  • 准备一个任务队列
  • 启动固定数量的 worker
  • 每个 worker 从队列里取任务执行
  • 主流程等待所有任务完成
 1jobs := make(chan int)
 2var wg sync.WaitGroup
 3
 4for i := 0; i < 4; i++ {
 5    wg.Add(1)
 6    go func(id int) {
 7        defer wg.Done()
 8        for job := range jobs {
 9            fmt.Println("worker", id, "job", job)
10        }
11    }(i)
12}

为什么它比“每个任务一个 goroutine”更稳

因为系统同时运行的任务数有了上限。你把上限设成 4,就意味着无论队列里堆了多少任务,真正并发执行的都只有 4 个。

一个完整点的收尾方式

1for _, job := range []int{1, 2, 3, 4, 5, 6} {
2    jobs <- job
3}
4close(jobs)
5wg.Wait()

这里的关闭动作很关键。worker 的循环通常是 for job := range jobs,所以你必须在任务发完后关闭通道,它们才能自然退出。

实战里常加的能力

超时和取消

worker 执行外部调用时,最好带上 context,这样任务不需要时能及时中止。

错误收集

可以单独准备一个 errCh,或者用 errgroup / 聚合器收集处理结果。

背压

如果 jobs 是有缓冲通道,缓冲区大小也在影响系统行为。缓冲过大时,生产者可能一次性堆太多任务;过小时,又可能过早阻塞。

我的总结

  • worker pool 的目标不是“更快”,而是“可控”
  • 它特别适合限流、批处理、后台消费
  • 收尾时别忘了关闭任务通道并等待 worker 退出
  • 如果任务会访问外部系统,记得把 context 一起传进去