直接用go func()会内存打爆,因go运行时不限制goroutine数量,1000次循环即1000个goroutine,每个占约2kb栈且加剧gc与调度压力;应使用带缓冲channel(如sem := make(chan struct{}, n))作信号量实现硬限流。

为什么直接 go func() 会把内存打爆
不是 Gin 框架的问题,而是 Go 运行时对 Goroutine 的调度不设上限。一个循环里写 go handler(c),1000 次就是 1000 个 goroutine;如果每个处理要开 DB 连接、读文件、解析 JSON,瞬时内存分配飙升,GC 来不及回收,服务就 OOM。你看到的“CPU 突增”“响应变慢”“dmesg 里有 oom-killer 日志”,基本都指向这个根因。
用带缓冲 channel 实现固定容量并发池
这是最轻量、无第三方依赖的方案,本质是用 chan struct{} 当信号量——容量即最大并发数,发送操作阻塞即限流。
- 创建容量为 N 的通道:
sem := make(chan struct{}, N) - 每个任务开始前
sem ,拿到“通行证”才执行 - 任务结束必须
(或用 <code>defer func(){ ),否则池子会耗尽 - 不要用
time.Sleep模拟耗时,它会挂起 goroutine 但不释放通道,导致死锁
示例片段:
sem := make(chan struct{}, 10) // 最多同时 10 个
for _, id := range ids {
sem <h3>别踩 <code>c.Copy()</code> 和上下文泄漏的坑</h3><p>如果你在 goroutine 里需要访问请求数据(比如 <code>c.Param("id")</code> 或 <code>c.Request.Body</code>),<strong>必须</strong>用 <code>c.Copy()</code> 复制上下文。原 <code>*gin.Context</code> 在 handler 返回后会被 <code>sync.Pool</code> 回收,后续访问会 panic 或读到脏数据。</p>
- 错误写法:
go func() { c.JSON(...) }()→ 崩溃概率极高 - 正确写法:
cc := c.Copy(); go func() { cc.JSON(...) }() -
c.Copy()会深拷贝请求头、参数、键值对,但不会复制原始http.Request的 body(已读过),所以异步任务里不能再调c.ShouldBindJSON()
何时该换 ants 或 errgroup
当并发池需要更精细控制(比如任务排队超时、动态扩缩容、统一 panic 捕获),ants 库比手写 channel 更稳;而 errgroup.Group 更适合“等全部完成并汇总错误”的场景。
- 用
ants:适合长期运行的后台任务池,支持SubmitWithTimeout、自动扩容/缩容 - 用
errgroup:适合并发调多个 API 并收集结果,天然支持ctx.WithTimeout - 别为了“看起来高级”引入
ants—— 简单批量处理,channel 就够了;复杂调度逻辑,再切过去
真正容易被忽略的是:并发池容量不能只看 CPU 核心数,得结合数据库连接池大小、下游接口 QPS、单次任务平均内存占用一起算。设成 50 可能刚起步就压垮 MySQL,设成 2 又浪费资源。上线前必须用真实数据压测验证。











