照着这篇文章走一遍,你就能用 pprof 抓出 Go 程序里最耗 CPU 和内存的那几行代码,并用基准测试验证优化到底有没有效果。
Go 自带一套性能分析工具链,pprof 就是入口。它不难,难的是很多人第一次用就卡在"数据怎么采"和"图怎么读"这两步。下面按顺序来。
第一步:让程序具备采集能力
Web 服务最简单,匿名导入即可:
import (
_ "net/http/pprof"
)
func main() {
go func() {
// 独立端口,不要和业务端口混用
http.ListenAndServe("127.0.0.1:6060", nil)
}()
// ... 你的业务逻辑
}
启动后访问 http://127.0.0.1:6060/debug/pprof/,能看到 profile、heap、goroutine、block、mutex 等入口。
命令行程序或单元测试用 runtime/pprof 手动写文件:
f, _ := os.Create("cpu.out")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
注意:6060 只监听 127.0.0.1 或走内网 + 鉴权。pprof 页面能暴露调用栈、函数名甚至部分内存内容,挂公网等于把代码结构摊开给人看。
第二步:分清楚四种 profile 各自看什么
- profile(CPU):默认采集 30 秒,看时间花在哪。
?seconds=60 可调整。
- heap(内存):分
inuse_space(当前占用)和 alloc_space(累计分配),用 ?sample_index= 切换。
- goroutine:看协程数量与阻塞位置,查泄漏必用。
- block / mutex:阻塞与锁竞争,默认不采,要先开:
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
这两个开关有性能开销,排查完记得关掉或改回 0。
注意:heap 是采样的,默认每分配 512KB 记一个样本,大量小对象分配可能被低估。要精确定位可以临时设 runtime.MemProfileRate = 1,但会很慢。
第三步:抓 CPU profile 并读 top
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30
进入交互后常用命令:
top # 按 CPU 占用排序,看 flat 和 cum 两列
list 函数名 # 直接定位到源码行,附带每行耗时
peek 函数名 # 看谁调用它、它又调用了谁
web # 生成 SVG 调用图(需装 graphviz)
flat 是本函数自身耗时,cum 是包含下游调用链的累计耗时。优化优先盯 flat 高的函数——那才是真正干活的地方;cum 高但 flat 低的多半只是"传话的"。
更省事的方式:go tool pprof -http=:8080 cpu.out,浏览器里直接看火焰图。
注意:采集期间程序会被轻微拖慢,线上高负载时段别采太久,30 秒足够;一次只改一个变量,否则你分不清是哪次改动生效了。
第四步:内存分析用"两次对比法"
只抓一个 heap 往往看不出问题,正确姿势是抓两个时间点再相减:
go tool pprof -base heap1.out heap2.out
在交互里执行 sample_index=alloc_space,然后 top。alloc_space 累计分配量大但 inuse_space 小的对象,通常是可以复用或用 sync.Pool 缓存的临时对象。
常见优化手段,按性价比排序:
- 预分配容量:
make([]T, 0, n),避免切片反复扩容拷贝
- 字符串拼接用
strings.Builder,别用 +=
sync.Pool 复用高频创建的中等对象
- 减少 interface 装箱:小结构体传值改传指针,避免逃逸到堆
- 锁粒度:把大锁拆成 map 分片锁,减少 mutex 竞争
第五步:goroutine 泄漏与锁竞争
go tool pprof http://127.0.0.1:6060/debug/pprof/goroutine
进去后输入 traces,能看到完整调用栈,一眼就能发现成千上万个协程卡在同一个 channel 接收或 HTTP 请求上——那就是泄漏点,通常是忘了 context 超时或 defer cancel()。
block profile 则用来回答"为什么整体吞吐上不去":如果大量时间卡在 channel 操作或 sync.WaitGroup,说明并发度设计有问题,不是 CPU 跑不动。
第六步:用 benchmark 精确定位并验证
线上不好复现的,就在测试里跑:
go test -bench=. -benchmem -cpuprofile=cpu.out -memprofile=mem.out
go tool pprof -http=:8080 cpu.out
-benchmem 会输出 allocs/op 和 B/op,这两个数字比耗时更稳定,特别适合判断"是不是少分配了"。优化前后各存一份结果,用 benchstat 对比:
go install golang.org/x/perf/cmd/benchstat@latest
benchstat old.txt new.txt
它会给出变化百分比和显著性判断,比你自己拍脑袋看耗时靠谱得多。
注意:看延迟别只看平均值。优化效果要结合 p99 看,很多"平均变快了"的改动实际上是把长尾拖得更长。
小结
- Web 服务
import _ "net/http/pprof" 即可,端口务必只对内网开放
- 四种 profile 各司其职:CPU 看热点、heap 看分配、goroutine 看泄漏、block/mutex 看竞争
- 读 CPU 图优先盯
flat 高的函数,用 list 函数名 直接定位到行
- 内存问题用两次 heap 采样相减,切
alloc_space 看累计分配
- block 和 mutex 默认不采集,需要手动设置采样率
- 优化效果用
go test -bench -benchmem + benchstat 验证,看 B/op 和 allocs/op
- 一次只改一处,改完立刻复测,别凭感觉优化