必须控制goroutine数量以防内存暴涨和系统崩溃;用带缓冲channel作信号量最轻量,缓冲大小即最大并发数,每次任务前写入struct{}获取令牌。

直接起成千上万个 go func() {}() 会炸——不是程序立刻 panic,而是内存涨、调度卡、文件描述符耗尽、下游服务被打挂。控制 goroutine 数量不是“要不要做”,而是“必须用有界机制兜住”。
用带缓冲 channel 做信号量最轻量
这是原生、无依赖、适合简单限流的方案:把 chan struct{} 当作令牌桶,缓冲大小即最大并发数。
- 每次任务开始前写入一个空结构体(
sem ),若 channel 满则阻塞,天然实现排队 - 任务结束必须用
defer func() { 归还,否则 token 泄漏,池子会慢慢“锁死” - 别用
make(chan struct{}, 0)(无缓冲)——那等于每次都要等另一个 goroutine 释放,实际变成串行 - 注意:该方式不管理 worker 生命周期,只管“同时最多跑几个”,适合短平快任务(如 HTTP 请求、DB 查询)
ants.NewPool() 参数选不对等于没限
ants.NewPool(n) 的 n 不是越大越好,也不是拍脑袋定的数字。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- IO 密集型(调 API、查 DB):按下游瓶颈算,比如目标服务 QPS=300、平均延迟=150ms → 理论并发 ≈ 300 × 0.15 = 45,填
50比较稳 - CPU 密集型(加解密、压缩):别超
runtime.NumCPU(),否则线程切换开销反超收益 - 混合型或不确定:从
10起步,用pprof看/debug/pprof/goroutine?debug=2和调度延迟曲线再调 - 顺手加上
ants.WithExpiryDuration(10 * time.Second),避免长周期服务里积攒一堆空闲但没退出的 goroutine
Submit 传函数时 panic 默认静默吃掉
这是 ants 最容易被忽略的坑:pool.Submit(func() { panic("oops") }) 不会崩,也不会打日志,错误直接消失。
- 必须显式注册处理函数:
ants.WithPanicHandler(func(p interface{}) { log.Printf("ants panic: %v", p) }) - 如果任务来自外部输入(如用户脚本、配置驱动逻辑),更建议在任务内部自己
recover,别指望池子兜底 - 注意:
WithNonblocking(true)下,提交失败(池满)也不返回 error,得靠监控指标或回调感知丢任务 - 别直接
pool.Submit(fn(arg))—— 这会立即执行,要传闭包:pool.Submit(func() { fn(arg) })
自研池子必须关 channel + 等 WaitGroup + recover
手写池子看似简单,但漏掉任意一环就可能泄漏或死锁。
- 任务 channel 必须在所有任务发完后
close(jobs),否则 worker 的for range jobs会永远阻塞 - 每个 worker 启动时
wg.Add(1),退出前wg.Done(),主 goroutine 用wg.Wait()等全部退出 - worker 内部必须包一层
defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r) } }(),否则单个 panic 会让整个 worker 退出,池子容量悄悄缩水 - 别在 worker 里直接
log.Fatal或os.Exit,这会干掉整个进程
真正难的不是“怎么写一个池子”,而是“怎么让池子在压测抖动、panic 频发、下游超时、服务重启这些真实场景下不掉链子”。参数值、panic 处理、关闭逻辑、监控埋点,缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










