直接用go启动协程易导致协程数暴增,引发内存溢出、调度失衡与GC频繁;应使用ants等协程池控制并发,注意初始化参数含义、及时释放、panic捕获及错误处理。

为什么直接用 go 启动协程会出问题
不加控制地写 go doWork(),短时间大量请求进来时,协程数可能飙升到几千甚至上万。Go 虽然轻量,但每个协程仍占 2KB+ 栈内存,加上调度开销和上下文切换,容易触发 GC 频繁、CPU 调度失衡,甚至 OOM。真实服务中常见现象是:接口响应延迟突增、runtime: failed to create new OS thread 报错、或 pprof 显示 goroutine 数持续 >5k。
workerpool 类库选型与初始化要点
社区主流选择是 panjf2000/ants 或 vishvananda/async,前者更成熟,后者极简。用 ants 时注意三点:
-
ants.NewPool(100)的参数是最大并发数,不是预创建数——协程按需启动,空闲超时(默认 60s)后自动回收 - 必须调用
pool.Release()关闭池,否则协程泄漏;若程序长期运行,可忽略释放,但测试时务必加defer pool.Release() - 任务函数不能直接 panic,需自行 recover,否则整个池可能卡住;建议统一包装:
func(task func()) { defer func(){recover()}(); task() }
如何把 HTTP handler 里的并发请求塞进池子
典型场景:一个 API 要并行查 3 个下游服务。别用 go callA() + go callB(),改用池提交:
func handler(w http.ResponseWriter, r *http.Request) {
pool, _ := ants.NewPool(50)
var wg sync.WaitGroup
results := make([]string, 3)
for i, fn := range []func() string{callA, callB, callC} {
wg.Add(1)
idx := i // 注意闭包捕获
_ = pool.Submit(func() {
defer wg.Done()
results[idx] = fn()
})
}
wg.Wait()
json.NewEncoder(w).Encode(results)
}
关键点:pool.Submit() 返回 error,当池已满且拒绝策略为 Deny 时会报 ants.ErrPoolOverload,线上应记录日志并降级处理,而不是 panic。
立即学习“go语言免费学习笔记(深入)”;
自定义协程池要注意的底层细节
如果不用第三方库,自己基于 chan 实现,有三个硬伤容易被忽略:
- 用
chan struct{}控制并发时,必须确保每个任务都从 channel 取 token 并在结束时归还,漏掉close()或defer就会导致后续任务永久阻塞 - channel 容量设为
N,不代表最多运行N个协程——若任务内又起go,实际并发会突破限制 - 无超时机制:某个任务卡死(如下游 hang 住),会一直占着 slot,整个池逐渐被耗尽;必须配合
context.WithTimeout包裹每个任务
真正稳定的池需要同时管控数量、生命周期、错误传播和可观测性,这也是为什么生产环境建议直接用 ants 而非手搓。


















