Go 用 pprof 定位性能瓶颈:CPU 火焰图与内存泄漏排查实战服务上线后 CPU 莫名飙高,或者内存一路涨到 OOM,靠加日志、猜代码往往找不到根因。Go 自带的pprof才是正解——它能告诉你 CPU 时间花在哪个函数、内存被谁一直占着不放。这篇不讲原理堆砌,直接带你把 pprof 接进服务、抓下数据、看懂火焰图、定位一个真实的内存泄漏。三行代码接入 pprof如果是 HTTP 服务,接入成本几乎为零。net/http/pprof包在 import 时会自动把一堆调试路由注册到默认的http.DefaultServeMux:packagemainimport(net/http_net/http/pprof// 匿名导入,只为触发它的 init() 注册路由)funcmain(){// 生产环境别把 pprof 挂在对外端口,单开一个内网端口gofunc(){http.ListenAndServe(localhost:6060,nil)}()// ... 你的业务服务select{}}访问http://localhost:6060/debug/pprof/就能看到所有可抓的 profile 类型。注意安全:pprof 端口暴露了会泄露内存内容、也能被拿来打 DoS,务必绑到localhost或内网、加鉴权,绝不能开到公网。抓 CPU profile 并看火焰图CPU profile 需要「采样一段时间」,因为它是按固定频率对正在运行的 goroutine 采样统计。抓 30 秒:# 采样 30s,期间对服务施加真实压力,数据才有代表性go tool pprof http://localhost:6060/debug/pprof/profile?seconds30进入交互式界面后,最常用两个命令:(pprof) top # 按累计耗时列出最贵的函数 (pprof) list 函数名 # 看某个函数逐行的耗时top输出里flat是函数自身消耗的时间,cum(cumulative)是它加上它调用的所有子函数的总时间。flat 高说明热点就在这个函数本身;cum 高但 flat 低说明它自己不慢,是它调的下游慢。但真正直观的是火焰图。用 web 命令直接在浏览器打开(需装 graphviz):go tool pprof-http:8080 http://localhost:6060/debug/pprof/profile?seconds30火焰图里:横条越宽 占 CPU 时间越多,纵向是调用栈(上面的函数被下面的调用)。你要找的就是最宽的那根「顶层平台」——那是耗时大头。别被塔尖迷惑,宽度才代表开销。排查内存:heap profile 的两种视角内存问题分两类,pprof 的 heap profile 用不同参数对应:# inuse_space(默认):当前still占用的内存 —— 查内存泄漏、常驻内存高go tool pprof http://localhost:6060/debug/pprof/heap# alloc_space:程序启动至今累计分配过的内存 —— 查 GC 压力大、频繁分配go tool pprof-alloc_spacehttp://localhost:6060/debug/pprof/heap这俩别搞混:排查「内存一直涨、疑似泄漏」用inuse_space,看现在到底是谁还占着内存不放;排查「GC 太频繁、CPU 被 GC 吃掉」用alloc_space,看谁在疯狂 new 对象制造垃圾。一个真实的内存泄漏:goroutine 泄漏Go 里最常见的「内存泄漏」其实是 goroutine 泄漏——goroutine 卡住不退出,它引用的内存也就永远回收不掉。看一段有问题的代码:funcleak(){ch:make(chanint)// 无缓冲 channelgofunc(){val:-ch// 一直等着收数据fmt.Println(val)}()// 函数直接返回了,没人往 ch 发数据// 那个 goroutine 就永远阻塞在 -ch,连同它的栈内存泄漏}每调一次leak()就泄漏一个 goroutine。这种泄漏在 heap 里不一定显眼,但在 goroutine profile 里一抓一个准:# 看当前所有 goroutine 的数量和它们卡在哪go tool pprof http://localhost:6060/debug/pprof/goroutine更快的办法是直接看计数——正常服务的 goroutine 数应该稳定,持续上涨就是泄漏信号:# ?debug1 直接返回可读文本,顶部就是 goroutine 总数curl-shttp://localhost:6060/debug/pprof/goroutine?debug1|head-1# goroutine profile: total 10482 - 这个数只涨不降,就是泄漏看到具体卡在-ch的调用栈后,修复就是给等待方一个退出路径,通常用 context:funcfixed(ctx context.Context){ch:make(chanint)gofunc(){select{caseval:-ch:fmt.Println(val)case-ctx.Done():// 上游取消时,goroutine 能退出,不再泄漏return}}()}对比两份 profile:揪出「谁在涨」排查缓慢的内存增长,最有效的手段是隔一段时间抓两份 heap,做差值对比:# 先存一份基线curl-shttp://localhost:6060/debug/pprof/heapbase.prof# 等 10 分钟,让泄漏累积curl-shttp://localhost:6060/debug/pprof/heaplater.prof# -base 做差:只显示这段时间新增的内存,泄漏点一目了然go tool pprof-basebase.prof later.prof-base差值对比把「一直就占很多」的正常内存滤掉,只留下「这段时间在涨」的部分,泄漏源直接浮出水面。这比盯着单份 profile 猜要高效得多。小结接入零成本:import _ net/http/pprof 单开一个内网端口即可,切记绑 localhost/内网并鉴权,别暴露公网。CPU:抓profile?seconds30(期间要有真实压力),用火焰图找最宽的条;top里 flat 是自身耗时,cum 是含子调用。内存:inuse_space查泄漏/常驻高,alloc_space查 GC 压力,两者别用错。Go 的「内存泄漏」多半是goroutine 泄漏;看goroutine?debug1的 total 是否只涨不降,修复靠给等待方加ctx.Done()退出路径。缓慢增长用pprof -base 旧 新做差值对比,直接看新增内存,别肉眼猜。一句话记忆点:CPU 看火焰图最宽条,内存分 inuse/alloc,泄漏先数 goroutine,增长用 -base 做差。