用goroutine+channel+select搭建固定worker池可稳控百万级并发,核心是限制worker数量、统一任务入队、配doneCh或context控制生命周期,避免goroutine泄漏与调度过载。

直接上结论:用 goroutine + channel + select 搭建固定数量的 worker,配合任务队列和关闭信号控制,就能稳住百万级并发压力下的资源消耗。关键不是“能起多少 goroutine”,而是“怎么不让它们失控”。
怎么定义一个可控的 Worker 池结构
别直接 go func(){}() 一把梭,得封装成可管理、可停止、可统计的实体。
-
taskCh是无缓冲或小缓冲的chan Task,所有任务统一入队,避免生产者阻塞太久 - 每个 worker 是独立
goroutine,死循环从taskCh接收任务,处理完再取下一个 - 必须配一个
doneCh chan struct{}或用context.Context控制生命周期,否则关不掉 - worker 数量建议设为
runtime.NumCPU() * 2起步,而非盲目设 100+ —— 过多 worker 反而加剧调度开销和缓存竞争
为什么不能让每个任务都起一个 goroutine
这是新手最常踩的坑:看到“Go 并发强”,就对每个请求或每条消息都 go handle(task)。结果是:
- goroutine 数量随流量线性暴涨,几分钟后突破 10 万,
runtime.NumGoroutine()持续上涨 → 典型 goroutine 泄漏 - 大量 goroutine 在 channel 上阻塞等待,调度器频繁切换,
pprof显示runtime.gopark占比飙升 - 内存分配暴增,GC 压力翻倍,
GOGC=100下每秒触发多次 GC,CPU 占用反升 - 错误日志里出现
runtime: goroutine stack exceeds 1000000000-byte limit或fatal error: stack overflow
如何安全关闭工作池并等待任务完成
关闭不是简单 close(taskCh) 就完事。要区分“不再接收新任务”和“等老任务跑完”两个阶段:
- 先关闭
taskCh,让 worker 的for task := range taskCh自然退出循环 - 但 worker 退出前要通知主协程:“我干完了”,靠
doneCh chan 实现 - 主协程用
for i := 0; i 等全部 worker 归位,再 return - 千万别用
time.Sleep硬等 —— 任务耗时不确定,要么等过头,要么没等够就退出
实际项目中容易被忽略的三个点
真正上线后出问题的,往往不是逻辑,而是这些细节:
- 任务函数内部没 recover panic,一个 panic 会让整个 worker goroutine 退出,池子悄悄少人
- channel 容量设太大(比如
make(chan Task, 10000)),内存吃紧且掩盖背压问题,该限流的时候没限 - 忘了在 worker 里加
ctx.Done()检查,遇到超时请求还在闷头处理,浪费资源
worker 池不是并发银弹,它解决的是资源可控性问题。复杂业务里,你大概率还得叠加 sync.WaitGroup 跟踪子任务、用 errgroup.Group 统一收集错误、甚至把 channel 换成带优先级的 ring buffer —— 但起点,一定是先让 goroutine 数量落在你心里那个数上。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











