应限制goroutine并发数以避免内存暴涨和响应延迟;每个goroutine占用2kb+栈空间,1000并发即耗2mb内存,且调度开销剧增;可用带缓冲channel如sem := make(chan struct{}, 20)实现20路并发控制。

goroutine 无节制启动导致内存暴涨和响应延迟
直接在每个 HTTP 请求里用 go 启动子任务,看似并发了,实则埋下雪崩隐患。Goroutine 虽轻量,但每个仍占 2KB+ 栈空间,1000 并发请求就可能吃掉 2MB 内存;调度器也会因 goroutine 数量激增而频繁切换,CPU 时间花在调度上而非业务逻辑。
- 用带缓冲的 channel 当信号量:比如
sem := make(chan struct{}, 20)控制最大并发数为 20,请求进来先sem ,处理完再 <code> - 避免在 handler 里直接
go db.Query(...)或go http.Post(...),除非你已确认下游能扛住且有超时兜底 - 如果必须批量发起请求(如查多个外部 API),优先用
sync.WaitGroup+ 固定数量 worker,而不是为每个请求开一个 goroutine
HTTP 客户端连接池没配,复用率低、建连耗时高
Go 标准库 http.Client 默认复用连接,但默认参数极保守:MaxIdleConns 是 100,MaxIdleConnsPerHost 是 2,IdleConnTimeout 是 30 秒。高并发下极易打满连接池,后续请求排队等空闲连接,表现就是响应时间毛刺明显、P99 拉高。
- 全局复用一个
*http.Client实例,不要每次请求都 new - 显式配置 Transport:把
MaxIdleConns设为 200~500,MaxIdleConnsPerHost至少设为 50,IdleConnTimeout建议 90 秒 - 如果用
gorequest等封装库,别只调Timeout(),一定要拿到底层Transport并重置连接池参数
Context 没传到底层 I/O,请求卡住不释放资源
常见错误是 handler 里用了 context.WithTimeout(r.Context(), 5*time.Second),但调数据库或发 HTTP 请求时又忘了把 context 传进去。结果是客户端已断开、超时已到,goroutine 还在等 DB 返回,变成“僵尸 goroutine”,内存和连接全被占着。
- 所有阻塞操作必须用带 context 的变体:
db.QueryContext(ctx, ...)、http.NewRequestWithContext(ctx, ...)、client.Do(req)前确保req = req.WithContext(ctx) - 不要用
time.AfterFunc或time.Sleep替代 context 超时,它们无法响应取消信号 - 下游服务返回 5xx 时,别盲目重试——先检查
ctx.Err()是否已 cancel,是则立即退出,否则可能重复消耗资源
sync.Mutex 在高频路径上锁粒度太粗
比如整个 handler 函数开头就 mu.Lock(),结尾 mu.Unlock(),所有并发请求串行执行,彻底失去并发意义。更隐蔽的是在 map 上读写加锁却不区分读写场景,读多写少时 sync.RWMutex 或 sync.Map 才是合理选择。
- 优先用
sync.Map替代map+sync.Mutex,尤其当 key 是字符串且读远多于写时 - 若必须用 mutex,锁只包裹真正共享修改的代码段,比如只锁
counter++那一行,而不是整个统计逻辑 - 避免在锁内做 I/O、网络调用或长耗时计算,否则其他 goroutine 会无限期等待
实际压测中,最常被忽略的是连接池参数与 context 传递的组合问题:哪怕 goroutine 控制得再好,只要一次 DB 查询没传 ctx,就可能拖垮整条链路。不是单点优化能解决的,得从请求入口到最深调用层层对齐。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











