直接用 go f() 启动协程会导致协程数失控、内存暴涨和调度开销大;Worker Pool 通过固定数量协程轮询消费带缓冲通道实现可控并发,worker 需用 task, ok := <-ch 判断通道关闭。

为什么直接用 go f() 启动协程会崩
任务量一上来,协程数就失控,内存暴涨、调度开销大、甚至被系统 OOM kill。Go 的协程虽轻量,但不是免费的——每个默认栈 2KB 起,上万协程光栈内存就吃掉几十 MB,更别说调度器频繁切换带来的延迟抖动。
Worker Pool 的核心就是「用固定数量的长期存活协程,轮询消费一个共享通道」,把并发控制收口到一个地方。
- 别用
for range tasks { go handle(task) }这种放养式写法 - 任务通道必须是带缓冲的(或至少是无缓冲但有明确关闭机制),否则生产者可能阻塞在发送上
- worker 协程要能感知通道关闭,避免死等 —— 用
task, ok := 判断
sync.WaitGroup 和 close() 怎么配着用才不 panic
常见错误是:任务通道关早了,worker 还在读;或者关晚了,WaitGroup 等不到所有 worker 退出,主协程提前结束。
关键顺序只有一条:所有任务发完 → 关闭任务通道 → 等所有 worker 退出 → 才算真正结束。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 关闭通道只能由**生产者**执行一次,别在 worker 里关
-
WaitGroup.Add()必须在启动 worker 前调用,且数值固定(比如 5 个 worker 就wg.Add(5)) - 每个 worker 结束前必须调用
wg.Done(),哪怕提前 return 也要 defer - 不要用
range遍历已关闭的通道来“自动退出”——它确实会退出,但你没法在退出前做清理(比如写日志、归还资源)
任务通道类型选 chan Task 还是 chan *Task
取决于 Task 大小和是否需要修改原值。小结构体(
但要注意:如果多个 worker 共享同一个 *Task,又没加锁,就会出现数据竞争 —— 这不是 Worker Pool 模式的问题,是你没管好数据所有权。
- 推荐默认用
chan Task,语义清晰,无共享风险 - 如果
Task含 slice/map/chan 字段,必须深拷贝或确认只读,否则传值也会出问题 - 别为了“看起来高效”强行传指针,Go 的逃逸分析很聪明,小对象传值未必比指针慢
怎么让 Worker Pool 支持动态扩缩容(其实不该这么干)
真需要弹性?说明你没分清「负载波动」和「设计缺陷」。Worker Pool 是为稳态吞吐服务的,不是为扛突发流量设计的。
动态增减 worker 意味着你要安全地启停 goroutine、重分配任务、处理中间状态 —— 这些逻辑远比池子本身复杂,而且容易引入竞态和泄漏。
- 突发流量该用缓冲队列 + 限流(如
golang.org/x/time/rate)挡在前面 - 长期负载上升?直接改初始化时的 worker 数,然后滚动发布
- 如果真要热调整,用原子变量控制 worker 是否继续循环,再配合新旧通道切换 —— 但上线前务必压测验证关闭路径
最常被忽略的一点:worker 里别藏阻塞调用(比如没设 timeout 的 HTTP 请求、没 context 控制的数据库查询),否则一个卡住的 worker 就等于整个池子少了一个 slot,而且你还发现不了。

















