pprof是Go性能分析核心工具,需导入"net/http/pprof"启用HTTP服务,通过/debug/pprof获取CPU、heap、goroutine等数据,用go tool pprof交互分析定位热点、内存泄漏与协程泄漏。

Go程序跑得慢,八成不是CPU卡住,而是GC在疯狂停顿——PauseTotalNs飙升、NumGC暴涨,goroutine调度直接卡死。别急着重写算法,先让程序自己“说话”。
用pprof暴露真实瓶颈,别猜热点
新手最容易对着fmt.Sprintf一顿优化,结果profile一看它只占0.2%耗时,而json.Unmarshal吃掉65%。性能问题必须靠数据定位,不是靠直觉。
- 启动时加一行:
import _ "net/http/pprof",再起个goroutine监听localhost:6060 - 压测后立刻抓三类关键数据:
— CPU热点:go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
— 内存分配大户:go tool pprof http://localhost:6060/debug/pprof/heap(重点看allocs_space)
— goroutine泄漏:go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 - 别跳过
go tool compile -gcflags="-m":查清变量是否逃逸到堆,比如&v在for range里反复取地址,就必然逃逸
高频分配场景,优先套sync.Pool和预分配
HTTP handler里每次make([]byte, 4096),一秒钟上千请求就是上千次堆分配,GC很快顶不住。但Pool不是万能膏药,乱用反而污染数据或浪费内存。
- 适合池化的对象只有两类:
— 短生命周期缓冲区(如[]byte、bytes.Buffer)
— 高频新建的结构体指针(如*RequestCtx、*JWTToken) - 必须全局复用Pool,不能每个handler自己new一个;
Put前务必清空字段,比如buf.Reset()、m = make(map[string]string),否则下次Get拿到的是脏数据 - 切片别只
make([]T, 0),明确长度就写make([]T, 0, N);map也一样,make(map[string]int, 100)比默认容量少一半扩容开销
goroutine泄漏比慢更危险
一个泄漏的goroutine不占CPU,但持续吃2KB栈内存、阻塞channel、拖慢GC扫描——运行一周后runtime.NumGoroutine()从300涨到30000,OOM只是时间问题。
立即学习“go语言免费学习笔记(深入)”;
- 常见泄漏点:
— HTTP handler里启goroutine但没传context,上游断连后goroutine永远卡在ch
— timer未Stop(),或time.AfterFunc绑定闭包导致引用无法释放
— channel无缓冲且读端已退出,写端一直阻塞 - 诊断方法:
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2看堆栈,重点关注长期停留在select、chan send、timer的地方 - 防御性写法:所有goroutine启动前绑定
ctx.Done(),用select { case 做快速退出检查
真正卡住服务的,往往不是某行代码多花了10μs,而是sync.Pool忘了清空字段、for range里取了错误地址、或者HTTP client没配Timeout——这些细节不显眼,但会在高并发下指数级放大。优化不是改完就完,是每次上线后都盯一眼PauseTotalNs和goroutine数。











