直接用 go f() 不行,因无节制启 goroutine 会压垮内存和调度器;worker pool 通过固定数量长期 goroutine 控制任务流,避免雪崩。

为什么直接用 go f() 不行,而要上 Worker Pool
因为无节制启动 goroutine 会压垮内存和调度器——尤其当任务量突增、每个任务又带 I/O 或计算开销时。runtime.GOMAXPROCS 不是并发上限,goroutine 数量可以轻松破万,但系统线程(OS thread)和上下文切换成本会指数级上升。Worker Pool 的本质不是“加速”,而是“控流”:把无限任务队列塞进固定数量的长期存活 goroutine 里,避免雪崩。
常见错误现象:fatal error: runtime: out of memory、大量 goroutine 卡在 select 或 chan recv、pprof 显示 goroutine 数量持续攀升不降。
- 适用场景:HTTP 请求批量处理、日志写入、数据库批量插入、第三方 API 调用聚合
- 不适用场景:单次、长时阻塞任务(如
time.Sleep(1h)),这类应单独协程 + 超时控制 - 池大小经验值:通常设为
runtime.NumCPU() * 2起步,再根据 CPU/IO 密集度调优;纯 CPU 任务别超 CPU 核数
workerPool 结构体里必须藏哪三个 channel
经典三通道模型不是炫技,每个 channel 承担不可替代的职责:任务入口、结果出口、关闭信号。少一个就会导致死锁或泄漏。
错误写法示例:只用一个 chan Job,结果处理全塞在 worker 内部 —— 无法统一收集返回值,也无法做失败重试或超时熔断。
立即学习“go语言免费学习笔记(深入)”;
-
jobs chan Job:无缓冲或小缓冲(如make(chan Job, 100)),防止生产者无限堆积 -
results chan Result:建议无缓冲,让 worker 同步汇报,避免结果丢失(若用缓冲,需配额外 goroutine 消费,增加复杂度) -
quit chan struct{}:唯一安全退出通道,worker 在 select 中监听它,收到即 return,不丢任务
注意:jobs 和 results 都不该用 close() 控制生命周期 —— 关闭 channel 会导致已关闭 channel 上的接收操作立即返回零值,极易掩盖真实错误。
如何避免 workerPool.Submit() 调用后任务“静默消失”
静默消失 = 任务发进 jobs 但没被消费,通常因为 worker 已退出、channel 缓冲满且未设超时、或 Submit 方法本身没做阻塞保护。
实操关键点:
- 提交前先
select判断jobs是否可写,加超时(如time.After(5 * time.Second)),超时就返回错误,别硬塞 - worker 内部必须用
select处理jobs接收 +quit监听,不能写成for job := range jobs—— 这样关不掉 - 启动 worker 时用
for i := 0; i ,别漏循环,也别用 <code>go p.worker写错成单个协程 - 如果任务需返回值,
Job类型里建议带done chan 字段,由 worker 写入,调用方用 <code>select等待,避免阻塞主线程
关闭 workerPool 时最常踩的两个坑
关闭不是调个方法就完事。真正难的是:等所有正在跑的任务结束,且确保没新任务挤进来,同时不让调用方 panic。
- 别在
Close()里直接close(p.jobs)—— 正在运行的 worker 可能刚读完一条任务,下一条还没读,此时 close 会让后续jobs panic:<code>send on closed channel - 正确顺序:先关
quit通道(close(p.quit)),再range读空results(如有),最后才允许Close()返回;worker 内部用select收到quit就 break for 循环,自然退出 - 如果用了带
done chan的任务模型,关闭前必须确保所有done都被消费,否则调用方可能永远阻塞在
最容易被忽略的点:pool 关闭后,Submit() 方法必须立刻返回错误(比如 ErrPoolClosed),而不是继续往已关闭的 jobs 发送 —— 这个判断得放在 Submit 开头,且要原子读取 pool 状态(用 sync/atomic 或 mutex)。










