Skip to content

Go 性能诊断工具选型:六个工具,六种不同的问题 ​

“给我一张火焰图”还不是一个足够明确的诊断请求。CPU 的火焰图、fgprof 的墙钟采样图和 heap 的分配图,宽度表示的东西不同;一段耗时很长的等待,在 CPU 图里可能完全不宽。选工具前,先写下要证实的判断:哪里耗 CPU、哪里等待、谁没退出、这次改动是否更快,还是某个变量为什么错了。

本文逐一说明 pprof、benchstat、dlv、fgprof、goleak 与 go tool trace,并用同一份本地工作负载比较 CPU 与墙钟采样。所有测量都限制在本地短命进程,没有生产压测、远程 attach 或公开诊断服务。

用问题选入口 ​

问题首选工具输入与输出不适合独自回答
CPU、分配或锁等待的热点在哪?pprofGo profile 文件 → top、调用图、火焰图任意变量值、完整时间先后
两版改动是否有可复核差异?benchstat重复基准文本 → 中位数、区间、A/B 比较一次线上慢请求为什么慢
某一行的状态与控制流是什么?dlv活进程或匹配 core → 断点、栈、变量无干扰的生产延迟测量
CPU 与不占 CPU 的等待,主要落在哪些栈?fgprof周期性 goroutine 栈采样 → pprof/folded 文件精确 CPU 消耗、每个请求的因果时间线
测试结束后哪些 goroutine 没结束?goleak测试进程的 goroutine 栈 → 泄漏诊断对未执行路径证明“没有泄漏”
被唤醒后何时运行,GC/阻塞发生在何时?go tool traceruntime trace → 时间线、调度等摘要远端数据库执行计划、所有业务函数耗时

可以组合使用,但不必同时采集。全量内存采样会改变分配成本,block 采样会影响调度,调试断点直接暂停进程。先用最便宜的证据缩小问题,再分开做更有针对性的采集。[1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    Q[把问题写成可检验的判断] --> R{主要缺哪种证据?}
    R -->|资源分布| P[pprof]
    R -->|混合计算与等待的栈| F[fgprof]
    R -->|先后与唤醒关系| T[go tool trace]
    R -->|当前变量与分支| D[dlv]
    R -->|结束后仍活着的任务| G[goleak]
    R -->|改动前后统计| B[benchstat]
    P --> M[形成假设并修改]
    F --> M
    T --> M
    D --> M
    G --> M
    M --> B
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    Q[把问题写成可检验的判断] --> R{主要缺哪种证据?}
    R -->|资源分布| P[pprof]
    R -->|混合计算与等待的栈| F[fgprof]
    R -->|先后与唤醒关系| T[go tool trace]
    R -->|当前变量与分支| D[dlv]
    R -->|结束后仍活着的任务| G[goleak]
    R -->|改动前后统计| B[benchstat]
    P --> M[形成假设并修改]
    F --> M
    T --> M
    D --> M
    G --> M
    M --> B

本文固定的版本 ​

工具本次版本验证深度
pprof、go tool traceGo 1.27.2 随附工具真实采集与命令行解析
benchstatx/perf v0.0.0-20261009192801-be2c69fb417e安装、10 份真实基准摘要
dlvv1.27.2安装、version、help;无断点/attach/core 实测
fgprofv0.9.5实际 API 编译、采集、pprof 解析、路由隔离测试
goleakv1.3.0检出受控未退出 worker,再清理并验证无残留

fgprof 的传递依赖 github.com/google/pprof 也固定为 v0.0.0-20240227163752-401108e1b7e7,完整 go.mod/go.sum 在文末。版本固定是为了复现,不能替代后续升级时的兼容和安全检查。

pprof:强项是资源归因,先选 sample type ​

pprof 既可以指 Go 的采集 API,也可以指分析器。runtime/pprof.StartCPUProfile/StopCPUProfile 采 CPU;heap、allocs、goroutine、block、mutex 等用内置 profile 导出;go tool pprof 分析文件。heap 默认看存活字节,allocs 默认看历史分配字节;block 看等待方,mutex 通常看引起竞争的临界区结束处,而且二者需要启用采样。[2]

本例一轮任务先 sleep 120 ms,再进行约 60 ms 的计算,共做两轮。waitIO 只是模拟不占 CPU 的等待,没有真的访问网络。CPU 采集在独立进程中运行,真实报告是:

text
Type: cpu
Duration: 360.82ms, Total samples = 110ms (30.49%)
110ms  100%  main.calculate

因此 calculate 占 CPU 样本 100%,但它没有占完整请求墙钟 100%。少掉的等待时间不意味着 CPU profiler 错,它本来就不负责统计 sleep 的墙钟等待。

bash
go tool pprof -top out/lab out/cpu.out

先看 flat(函数自身),再看 cum(含子调用)。火焰图宽度是选中指标的样本权重,横向不是时间轴。要看调用先后,换 trace;要看某个变量,换调试器。采样成本随 profile 类型、采样率和工作量变化,不存在所有服务都适用的“零开销”。[1][2]

fgprof:把不占 CPU 的等待也带进来 ​

fgprof v0.9.5 在后台 goroutine 里按目标 99 Hz 取 runtime.GoroutineProfile,记录正在运行和正在等待的 goroutine 栈,再导出墙钟采样权重。这个实现与 CPU profiler 的采样机制不同,因此不要把两者数字直接相减当作精确的 I/O 时间。[3]

最小本地入口是:

go
stop := fgprof.Start(f, fgprof.FormatPprof)
// 运行有界工作负载
if err := stop(); err != nil { return err }

停止函数会完成导出,应调用一次并处理错误,然后关闭文件。本例在另一个进程运行相同结构的工作负载,采得:

text
Type: time
Duration: 361.68ms, Total samples = 360.82ms (99.76%)
main.waitIO      cum=237.11ms  65.71%
main.calculate   cum=123.71ms  34.29%

上面两行摘出 cum 列并重新排版,完整 top 保留在复现输出。现在等待路径出现了,粗略比例与两轮 120 ms 等待、60 ms 计算相符。但具体几十微秒或某条 CPU 指令的归因不应靠它判断。本次 flat 里还出现 time.Now、runtime.asyncPreempt2,进一步说明这是取栈时的观测,不是逐指令的 CPU 计费。

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    W[同一结构的本地任务<br/>等待 120ms + 计算 60ms,重复两轮] --> C[CPU profiler<br/>只统计正在消耗 CPU 的栈]
    W --> F[fgprof<br/>周期性观察运行与等待的 goroutine 栈]
    C --> CR[本次 calculate 110ms CPU 样本]
    F --> FR[本次 waitIO 237.11ms<br/>calculate 123.71ms 墙钟权重]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    W[同一结构的本地任务<br/>等待 120ms + 计算 60ms,重复两轮] --> C[CPU profiler<br/>只统计正在消耗 CPU 的栈]
    W --> F[fgprof<br/>周期性观察运行与等待的 goroutine 栈]
    C --> CR[本次 calculate 110ms CPU 样本]
    F --> FR[本次 waitIO 237.11ms<br/>calculate 123.71ms 墙钟权重]

图中的两个采集是分开运行,不能用它们衡量“开启 fgprof 多花了多少 CPU”。如果要测开销,需要另外设计同等条件下开与关的重复比较。

fgprof 的代价随 goroutine 和栈增长 ​

它要反复遍历 goroutine 栈,goroutine 越多、不同栈越多,采样与存储成本越需要关注。采样 goroutine 自己也可能调度延迟;上游源码会按实际采样次数调整频率估计,但不能保证消掉所有偏差。大量空闲 worker 也会贡献等待权重,所有 goroutine 的权重相加可以超过采集窗口,所以不能默认每张图都代表单个请求。[3]

v0.9.5 用固定长度栈记录聚合,深调用栈与版本相关的栈展开行为也应验证。本文小例子可成功解析,不构成对所有 Go 1.27.2 应用栈形状的全面验证。

HTTP 入口是 fgprof.Handler(),当前参数 seconds 默认 30,format 默认 pprof,也支持 folded。它不替你提供认证、并发采集限制或生产预算。示例只注册在单独管理 mux:

go
m := http.NewServeMux()
m.Handle("GET /debug/fgprof", fgprof.Handler())

测试断言管理路由 POST 返回 405、全新的业务 mux 上 GET 返回 404。本文没真正开启 HTTP listener。上线还要加管理网络、身份校验、时长上限、并发限制和产物访问控制,不要把上游最短演示里的公开端口直接照搬。[3]

go tool trace:需要时间与因果时再用 ​

runtime/trace.Start/Stop 记录 goroutine 状态转换、系统调用、GC、P 活动等运行时事件;go tool trace 解析。它能区分 Waiting、被唤醒后的 Runnable、真正 Running,适合调查调度延迟、GC 停顿和同步等待。task/region/log 可进一步把事件和业务操作联系起来。[4]

同一程序的第三个独立进程采集 trace,实际执行:

bash
go tool trace -pprof=sched out/trace.out > out/sched.pprof
go tool pprof -top out/sched.pprof

本次成功得到 Type: delay 的调度摘要,总量约 350.21 μs。它不是那 240 ms 的 sleep 总量,因为 sched 摘要统计的是可运行之后等执行的调度延迟,而不是所有墙钟等待。工具还可导出 net、sync、syscall 汇总;交互时间线通过 go tool trace -http=127.0.0.1:8081 out/trace.out 查看。[4]

trace 记录事件的数量通常远大于一个聚合 profile,文件规模、内存和解析成本也随活动增长。限制窗口、保护标记中的业务数据,并用相应工具链解析。本篇验证了离线导出,没有声称打开浏览器渲染时间线,也没有把进程内 trace 说成自动跨服务链路追踪。

goleak:把“关闭之后该退出”写成测试约束 ​

goleak 检查测试进程里是否留下非预期 goroutine。常用方式是在单个串行测试末尾 goleak.VerifyNone(t);整个套件可用 goleak.VerifyTestMain(m)。先让组件有明确的 stop/cancel/Close,再等待它退出,否则工具只能指出没结束,不能替你设计生命周期。[5]

本例故意启动一个等 channel 的 worker,先让 goleak.Find() 返回诊断,再关闭 channel、等待 done,最后 VerifyNone:

go
stop := make(chan struct{})
done := make(chan struct{})
go leakedWorker(stop, done)
err := goleak.Find() // 此时应检出未退出 worker
close(stop)
<-done
goleak.VerifyNone(t)

实际测试日志:

text
TestLeakDetectedAndCleaned
leakedWorker detected=true
PASS

完整测试不仅检查 err 非 nil,还检查诊断包含 leakedWorker,避免把其他后台活动误当作目标反例。故意留下的 worker 随后确实清理,因此测试成功不靠真的泄漏到进程退出。另一测试验证 context 取消后 worker 在限定时间内退出。普通与 race 测试各重复两次,都通过。

不要在相互并行的每个测试里随意 VerifyNone:别的测试正在运行的 goroutine 可能造成误报。并行套件更适合在 TestMain 的整体结束点检查。IgnoreTopFunction、IgnoreAnyFunction 与 IgnoreCurrent 都会扩大忽略范围,只针对确实长寿、由其他生命周期管理的组件使用,并留下理由。把业务 worker 全部忽略,会让测试失去意义。[5]

goleak 要取栈,并会给正常退出留出重试等待,失败测试会更慢;本例检出阶段每次约 0.45 秒。它也不证明未运行的请求路径安全,更不等同于数据竞争检测或生产泄漏监控。

benchstat:比较的是重复测量,不会修复错误实验 ​

benchstat 读取 go test -bench 格式文本。默认给中位数与置信区间;输入多个可比版本时,会做统计比较。先确保输入、工具链、CPU 配置与测试语义相同,再保留足够多样本、交错前后运行,不要筛选最好看的几行。[6]

为了独立复现,本文附一个求和基准:对同一份 1024 个整数,用 b.Loop 保留被测计算,输入准备在循环前。Go 1.27.2 支持 B.Loop 的当前防消除语义。标准优化构建、AMD EPYC 9V74、GOMAXPROCS=1、100 ms/样本,共 10 份,实际摘要:

text
Add    608.4 ns/op ± 5%
       0 B/op
       0 allocs/op

这里只给单组摘要,没有第二种实现,所以不存在“比旧版显著更快”的结论。A/B 使用方式是 benchstat before.txt after.txt,两个文件里必须真的是同名、同工作量基准。默认统计是非参数摘要与 Mann–Whitney U 检验,~ 表示没检出显著差异,不能说两者已经被证明相等;显著也不表示收益在业务上够大。[6]

ns/op 是操作平均值,不能直接当 p99。共享机器上的短微基准容易受其他任务影响;race 或调试构建的数字不能和普通优化构建混比。工具成本主要发生在测试工作量与统计分析阶段,不必把 benchstat 接进运行中的服务。

dlv:当你真正需要看变量时 ​

Delve 可以断点、条件断点、单步、选择 goroutine、查看调用栈与变量,也能用匹配二进制离线检查受支持的 core。典型入口是 dlv debug .、dlv exec ./app.debug、dlv core ./app ./core.file。这与采样器最大的区别,是它能暂停和控制目标,因而也最容易改变时序。[7]

本地逻辑调试常用 go build -gcflags='all=-N -l' 构建;不能把这个调试构建当性能基线。优化构建的变量可能被消除或移到难以呈现的位置。查看 core 要用原始二进制,不能重新编一个同名程序去匹配旧地址。

本次只实际核对了:

text
Delve Debugger
Version: 1.27.2

version/help 成功不等于有目标进程调试权限。本环境不允许 ptrace,没有尝试 attach、运行断点或打开 core。生产使用前必须明确暂停影响、权限与恢复方式;core 和调试端口可能暴露整个进程的秘密,不能当普通性能图随意分享。[7]

如何组合,什么时候收手 ​

一个常用顺序是:监控确认症状 → pprof/fgprof 缩小资源或等待路径 → trace 澄清时序,或在授权副本上用 dlv 查状态 → 用测试与 goleak 固定生命周期 → 用重复基准和代表性系统负载验证修改。

不是每次都走完整条路线。找到“未关闭的 Rows 导致连接池排队”,就应修复归还路径并复验;再追一个无关纳秒热点只会分散精力。完成条件也应具体:相同场景的结果正确,目标指标改善,goroutine/资源能回落,没有把 CPU 成本换成不可接受的内存或尾延迟。

依据 ​

  1. Go diagnostics:各诊断类别与工具互相干扰。
  2. Go 1.27.2 runtime/pprof:各 profile、采样与归因。
  3. fgprof v0.9.5、采样源码、HTTP handler:99 Hz、GoroutineProfile、格式、生命周期与参数。
  4. Go 1.27.2 runtime/trace、cmd/trace:事件与 CLI 汇总;本次实际导出验证。
  5. goleak v1.3.0 API、固定实现:Find、VerifyNone、VerifyTestMain、忽略与重试。
  6. benchstat 官方文档、固定提交 be2c69fb417e;Go 1.27.2 testing。
  7. Delve v1.27.2 官方文档:断点、exec、core 与版本限制。

完整复现文件与命令 ​

需要 Go 1.27.2、Linux 和 GNU timeout。先按下列 go.mod/go.sum 下载固定依赖,之后测试与采样都在本地,不请求任何业务网站:

bash
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
bash run.sh out
mkdir -p bin
GOBIN="$PWD/bin" GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
  go install golang.org/x/perf/cmd/benchstat@v0.0.0-20261009192801-be2c69fb417e
./bin/benchstat out/bench.txt

Delve 版本/帮助检查独立于上述实验,没有启动目标程序:

bash
GOBIN="$PWD/bin" GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
  go install github.com/go-delve/delve/cmd/dlv@v1.27.2
./bin/dlv version
./bin/dlv help exec
./bin/dlv help core

go.mod ​

go.mod
module example.com/toolslab

go 1.27.2

require (
	github.com/felixge/fgprof v0.9.5
	go.uber.org/goleak v1.3.0
)

require github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect

go.sum ​

代码块 · 36 行 · 点击收起 / 展开
text
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go ​

代码块 · 84 行 · 点击收起 / 展开
go
package main

import (
	"fmt"
	"github.com/felixge/fgprof"
	"net/http"
	"os"
	"runtime/pprof"
	"runtime/trace"
	"time"
)

var value uint64

//go:noinline
func calculate() {
	until := time.Now().Add(60 * time.Millisecond)
	x := uint64(1)
	for time.Now().Before(until) {
		for range 10000 {
			x = x*1664525 + 1013904223
		}
	}
	value = x
}

//go:noinline
func waitIO() { time.Sleep(120 * time.Millisecond) }
func run(mode, path string) error {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	var stop func() error
	switch mode {
	case "fgprof":
		stop = fgprof.Start(f, fgprof.FormatPprof)
	case "cpu":
		if err := pprof.StartCPUProfile(f); err != nil {
			f.Close()
			return err
		}
		stop = func() error { pprof.StopCPUProfile(); return nil }
	case "trace":
		if err := trace.Start(f); err != nil {
			f.Close()
			return err
		}
		stop = func() error { trace.Stop(); return nil }
	default:
		f.Close()
		return fmt.Errorf("unknown mode %q", mode)
	}
	for range 2 {
		waitIO()
		calculate()
	}
	err = stop()
	closeErr := f.Close()
	if err != nil {
		return err
	}
	if closeErr != nil {
		return closeErr
	}
	fmt.Println(mode, "capture complete")
	return nil
}
func main() {
	if len(os.Args) != 3 {
		fmt.Fprintln(os.Stderr, "usage: lab fgprof|cpu|trace output")
		os.Exit(2)
	}
	if err := run(os.Args[1], os.Args[2]); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func diagnosticsMux() *http.ServeMux {
	m := http.NewServeMux()
	m.Handle("GET /debug/fgprof", fgprof.Handler())
	return m
}

main_test.go ​

代码块 · 82 行 · 点击收起 / 展开
go
package main

import (
	"context"
	"go.uber.org/goleak"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

//go:noinline
func leakedWorker(stop <-chan struct{}, done chan<- struct{}) { <-stop; close(done) }
func TestLeakDetectedAndCleaned(t *testing.T) {
	stop := make(chan struct{})
	done := make(chan struct{})
	go leakedWorker(stop, done)
	err := goleak.Find()
	if err == nil {
		close(stop)
		<-done
		t.Fatal("expected diagnostic")
	}
	if !strings.Contains(err.Error(), "leakedWorker") {
		close(stop)
		<-done
		t.Fatal(err)
	}
	t.Log("leakedWorker detected=true")
	close(stop)
	<-done
	goleak.VerifyNone(t)
}
func TestWorkerStops(t *testing.T) {
	defer goleak.VerifyNone(t)
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() { defer close(done); <-ctx.Done() }()
	cancel()
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("worker did not stop")
	}
}

func add(values []int) int {
	total := 0
	for _, v := range values {
		total += v
	}
	return total
}
func TestAdd(t *testing.T) {
	if add([]int{1, 2, 3}) != 6 {
		t.Fatal("add")
	}
}
func BenchmarkAdd(b *testing.B) {
	xs := make([]int, 1024)
	for i := range xs {
		xs[i] = i
	}
	b.ReportAllocs()
	for b.Loop() {
		_ = add(xs)
	}
}

func TestRouting(t *testing.T) {
	w := httptest.NewRecorder()
	diagnosticsMux().ServeHTTP(w, httptest.NewRequest("POST", "/debug/fgprof", nil))
	if w.Code != 405 {
		t.Fatal(w.Code)
	}
	w = httptest.NewRecorder()
	http.NewServeMux().ServeHTTP(w, httptest.NewRequest("GET", "/debug/fgprof", nil))
	if w.Code != 404 {
		t.Fatal(w.Code)
	}
}

run.sh ​

bash
#!/usr/bin/env bash
set -euo pipefail
export TZ=UTC
out=${1:-out}
mkdir -p "$out"
{ go version; go env GOOS GOARCH; grep -m1 'model name' /proc/cpuinfo; } > "$out/environment.txt"
go test -count=2 -v . > "$out/test.txt"
go test -race -count=2 . > "$out/race.txt"
go vet . > "$out/vet.txt"
go build -o "$out/lab" .
for mode in fgprof cpu trace; do
 timeout 5s "$out/lab" "$mode" "$out/$mode.out" > "$out/$mode-run.txt"
done
for mode in fgprof cpu; do
 go tool pprof -top "$out/lab" "$out/$mode.out" > "$out/$mode-top.txt"
done
go tool trace -pprof=sched "$out/trace.out" > "$out/sched.pprof"
go tool pprof -top "$out/sched.pprof" > "$out/sched-top.txt"
go test -run='^$' -bench='^BenchmarkAdd$' -benchmem -benchtime=100ms -count=10 -cpu=1 > "$out/bench.txt"
go version -m "$out/lab" > "$out/modules.txt"

本次十份原始求和基准 ​

text
BenchmarkAdd 	  178407	       659.7 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  206732	       614.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  197844	       610.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  186723	       636.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  183027	       615.4 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  206557	       596.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  197410	       594.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  196628	       604.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  209517	       601.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkAdd 	  186458	       605.9 ns/op	       0 B/op	       0 allocs/op

保留各书来源与许可 · 示例运行条件以正文为准