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一起传进去