必须在任务分发入口设并发闸门,不能依赖自动回收;用带缓冲的 channel(chan struct{})作信号量,缓冲大小即最大并发数,任务前写入 sem 获取令牌。

必须在任务分发入口设并发闸门,不能靠“等它自己结束”来指望回收。
用带缓冲 channel 做信号量最直接
这是原生、无依赖、适合短平快任务(如 HTTP 请求、DB 查询)的方案:把 chan struct{} 当作令牌桶,缓冲大小即最大并发数。
- 每次任务开始前写入
sem ,channel 满则阻塞,天然排队 - 必须用
defer func() { 归还,否则 token 泄漏,池子会慢慢“锁死” - 别用
make(chan struct{}, 0)(无缓冲)——那等于每次都要等另一个 goroutine 释放,实际变成串行 - 该方式不管理 worker 生命周期,只管“同时最多跑几个”,适合已知耗时可控的场景
ants.Pool 初始化参数不能拍脑袋定
ants.NewPool(n) 的 n 不是越大越好,也不是随便填个 100 就完事。
- IO 密集型(调 API、查 DB):按下游瓶颈算,比如目标服务 QPS=300、平均延迟=150ms → 理论并发 ≈ 45,填 50 比较稳
- CPU 密集型(加解密、压缩):别超
runtime.NumCPU(),否则线程切换开销反超收益 - 混合型或不确定:从 10 起步,上线后用
/debug/pprof/goroutine?debug=2和调度延迟曲线再调 - 顺手加上
ants.WithExpiryDuration(10 * time.Second),避免长周期服务里积攒一堆空闲但没退出的 goroutine
goroutine 内部必须响应 context 取消
漏掉这一条,限流和池子都白搭——你拦住了新请求,但旧 goroutine 卡在 http.Do 或 db.Query 上不退,runtime.NumGoroutine() 就只增不减。
- 启动 goroutine 时显式传入
ctx,别依赖闭包捕获外层变量(容易误用已取消的 context) - 所有阻塞操作都要支持 cancel:
http.NewRequestWithContext(ctx, ...)替代裸http.NewRequest;数据库查询必须用QueryContext,Query不受 context 控制 - timer/ticker 启动后不会自动 stop,每次
time.NewTicker后务必配defer ticker.Stop(),且注意 panic 时 defer 不执行,建议在 select 中监听ctx.Done()并显式 stop - 别在 goroutine 里用
time.After做超时,它不参与 context 取消
HTTP handler 入口必须加 rate.Limiter + context 超时
客户端发包过快不会立刻 panic,但会 1 秒起 5000 个 goroutine,每个占 ~2.6KB 栈 + 堆 buffer,10 秒就吃掉 1GB+ RSS。根本防线不在业务层,而在第一行 handler 代码。
- 用
golang.org/x/time/rate.Limiter,每秒允许 N 个 token,burst设为 2×N 防脉冲 - handler 开头就调
if !limiter.Allow() { http.Error(w, "too many requests", http.StatusTooManyRequests); return } - 别用
time.Sleep等 token,它会阻塞 goroutine,加剧堆积 - 所有外发 HTTP 调用必须传入带 timeout 的
context.Context,client 还要设Timeout和Transport.IdleConnTimeout
真正难防的不是并发高,而是 goroutine 启了却卡住不退——pprof 里看到成百上千个 [IO wait] 或 [select] 状态,基本就是 context 没传到底、channel 没关、ticker 没 stop 这三类问题之一。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











