应使用 golang.org/x/sync/semaphore 控制 goroutine 并发数,避免 channel 令牌池的超时释放、panic 未归还等缺陷,并按资源维度隔离信号量;复用 http.Client 并合理配置连接池;用 sync.Pool 缓存高频短生命周期对象;通过 pprof 定位真实瓶颈。

用信号量(semaphore)控制 goroutine 并发数,别靠 channel 硬扛
无节制启动 goroutine 是高并发服务崩溃的第一推手——不是 Go 不行,是没管住数量。用 make(chan struct{}, N) 做令牌池看似简单,但容易漏掉超时释放、panic 后未归还、或 channel 关闭后阻塞接收等坑。更稳妥的做法是直接用 golang.org/x/sync/semaphore:sem.Acquire(ctx, 1) 支持上下文取消和超时,panic 时也能靠 defer sem.Release(1) 保证归还。
- 并发上限建议设为 50–200,具体看下游承载力(如数据库连接池、第三方 API QPS)
- 不要在 handler 里直接
go f(),必须套上信号量或 worker pool - 避免把
sem定义成全局变量却在不同业务逻辑中混用,应按资源维度隔离(如 DB 调用一套、HTTP 调用另一套)
复用 http.Client 和连接池,别每次 new 一个
高频 HTTP 请求下,http.DefaultClient 的默认连接池参数(MaxIdleConns=100, MaxIdleConnsPerHost=100)常被低估——它只控制空闲连接,不等于能同时发起的请求数;而反复 &http.Client{} 会创建独立 Transport,导致连接池失效、文件描述符暴涨、甚至 dial tcp: lookup xxx: no such host 这类 DNS 缓存问题。
- 全局复用一个
*http.Client实例,配置Transport.MaxIdleConns=200、MaxIdleConnsPerHost=200、IdleConnTimeout=30s - 对关键外部调用,单独配一个 client(比如专用于支付回调),避免慢接口拖垮健康检查
- 务必设置
Timeout或用context.WithTimeout传入Do(),否则 goroutine 会卡在read系统调用上不释放
用 sync.Pool 缓存 buffer 和结构体,别让 GC 在高峰期抖三抖
pprof 抓到的高频分配点,80% 都集中在 bytes.Buffer、strings.Builder、JSON 解析后的 map[string]interface{} 或自定义请求结构体。这些对象生命周期短、创建频次高,GC 每次 STW 都可能让 P99 延迟突增 20ms+。
- 对固定大小的 buffer,用
sync.Pool复用:var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 0, 4096) }} - 避免在热路径用
fmt.Sprintf,改用strings.Builder+builder.Grow()预分配 - struct 实例也可池化,但注意清零字段(尤其指针、slice、map),否则残留数据引发脏读
用 pprof 定位真实瓶颈,别靠猜和调参数
很多优化失败,是因为在错误的地方使劲:比如拼命调大 MaxOpenConns,结果 pprof 显示 90% 时间耗在 json.Unmarshal;或者给所有 handler 加 go,却发现 runtime.gopark 占比飙升——说明 goroutine 在 channel 上排队等锁。
立即学习“go语言免费学习笔记(深入)”;
- 上线前必开
net/http/pprof,压测时抓/debug/pprof/profile?seconds=30(CPU)、/debug/pprof/heap(内存)、/debug/pprof/goroutine?debug=2(协程栈) - 重点关注
http.(*ServeMux).ServeHTTP下游函数,而不是顶层框架代码 - 用
go tool pprof -http=:8080可视化火焰图,一眼看出谁在抢锁、谁在等 I/O











