直接用 goroutine 并发易耗尽内存或压垮服务,需用 worker pool 限流;核心是任务入队、固定 worker 数取任务、结果通知;应定义具体 Job 结构体,用 jobs chan Job 和 results chan *Job,正确关闭 channels 并用 sync.WaitGroup 优雅退出。

为什么直接用 goroutine 会出问题
大量并发启动 goroutine 容易耗尽内存或压垮下游服务,比如读取 10 万条 URL 并发请求,若不加限制,go fetch(url) 可能瞬间创建 10 万个 goroutine。Go 运行时不会帮你限流,调度器只管复用,但资源(文件描述符、连接数、CPU 切换开销)是实打实的。
worker pool 的核心不是“怎么起 goroutine”,而是“怎么控制同时跑几个”。关键在:任务入队、固定数量 worker 持续取任务、任务完成通知。
chan 作为任务队列和信号通道的正确用法
别用 chan interface{} 存任务——类型擦除带来运行时断言开销,且无法静态检查。推荐定义具体任务结构体:
type Job struct {
ID int
Data string
Result *string
}
worker 池通常需要两个 channel:
立即学习“go语言免费学习笔记(深入)”;
-
jobs chan Job:无缓冲或带小缓冲(如make(chan Job, 100)),避免生产者阻塞太久 -
results chan *Job:用于返回结果,worker 处理完后写入,主 goroutine 从该 channel 收集
注意:close(jobs) 应由生产者调用,worker 需用 for job := range jobs 安全退出;而 results 不应 close,除非你确定所有 worker 已退出且不再写入。
如何优雅关闭 worker pool 并等待全部完成
常见错误是用 sync.WaitGroup + go func(){...}() 启动 worker,但忘记在 worker 内部 defer wg.Done(),或过早调用 wg.Wait() 导致主线程卡死。
更稳妥的做法:
- 启动 N 个 worker,每个 worker 启动前
wg.Add(1) - worker 函数结尾必须
defer wg.Done() - 生产者发完所有
Job后close(jobs) - 主 goroutine 调用
wg.Wait()等待所有 worker 退出,再close(results)(如果需要)
不要依赖 time.Sleep 或轮询判断,那是竞态源头。
实际使用中容易被忽略的边界点
worker pool 不是银弹,以下情况需额外处理:
- 任务执行可能 panic:worker 内部要包
defer func(){recover()}(),否则一个 panic 会让整个 worker 退出,导致任务丢失 - 任务超时控制:单个
Job应自带context.Context字段,worker 执行时用select { case 响应取消 - 结果收集顺序不保证:channel 是 FIFO,但 worker 执行时间不同,
results返回顺序 ≠ 提交顺序;如需保序,得在Job中带序号,主 goroutine 自行排序 - goroutine 泄漏风险:如果
jobschannel 没 close,而主 goroutine 已退出,worker 会永远阻塞在range上——务必确保 close 时机明确
真正难的不是写一个能跑的 pool,而是让它在失败、超时、重启、信号中断等场景下不丢任务、不泄漏、不假死。










