关于 go-performance-review
通过分析和测量来调查及改善 Go 性能的技能,指导架构与热点审查、基准测试驱动的改动,以及运行时、内存、I/O 和日志分析。
主要功能
- 01架构、热点与优化模式
- 02使用 benchstat 对比基准测试
- 03按症状选择性能分析方法
- 04审查内存、分配与 I/O
- 05运行时调优和日志建议
适用场景
- 01查找 CPU 或内存占用偏高的原因
- 02将优化效果与基线进行比较
- 03审查 HTTP、内存分配或日志开销
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-performance-review安装后在 Claude Code 中输入/go-performance-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/benchmarking.mdlogging.mdmemory-and-io.mdprofiling.mdruntime-tuning.md详细介绍
Go 性能审查
先分析,再优化。只有基准测试证明存在问题时,才进行优化。
详细参考资料按需加载:
references/benchmarking.md— 基准测试结构、运行与解读 benchstat、比较不同方案、提交格式、CI 回归检查。references/memory-and-io.md— 分配、热路径和映射示例、 内存保留、结构体布局、伪共享、HTTP 客户端、流式处理、批处理。references/profiling.md— 不同症状对应的性能分析类型、采集 命令、解读 pprof、编译器诊断、跟踪、飞行记录器。references/runtime-tuning.md— GOGC/GOMEMLIMIT、解读gctrace、 容器中的 GOMAXPROCS、PGO。references/logging.md— slog 与 zap/zerolog 决策表、使用 zap 后端的 slog、热路径中的日志反模式。
只有以下摘要不足以解决问题时,才阅读参考文件。
操作模式
- 架构审查(一个包或服务):查找结构性开销——未复用连接、 无限制的 goroutine、N+1 查询、错误的数据结构、可以一次完成却按请求重复执行的工作。分别审查分配与布局、I/O 与并发,以及算法与缓存;如果环境支持,为每项审查安排一个并行子 Agent。
- 热路径审查(调用者指定的一个函数):结合其性能分析结果,按顺序阅读。
- 优化(已知瓶颈):按以下周期执行,每次只做一项改动。
优化周期
- 明确指标和目标——p99 延迟、吞吐量、
allocs/op、RSS。没有目标,优化就不会结束。 - 先排除外部耗时。 如果 goroutine 停在
net.(*conn).Read或database/sql,或者跟踪中有一个上游跨度占据主导地位,就说明应在别处修复(go-database、go-observability)。 - 对隔离路径进行基准测试并保存基线:
go test -run '^$' -bench Name -benchmem -count=10 | tee old.txt。 - 通过性能分析找到原因——见下文“性能分析”。
- 只改一项内容,并添加注释说明这样做为何更快。
- 在同一台机器和同一工具链上重新测量;只有当
benchstat old.txt new.txt显示出显著差异时,才保留此改动。 - 将 benchstat 表格放入提交正文(
perf(scope): ...)。
1. 减少分配
- 单个值使用
strconv,而不是fmt.Sprintf。 - 循环中使用
strings.Builder(已知大小时调用Grow),而不是+=。 - 已知大小或有界时,预分配切片和映射:
make([]T, 0, n)、make(map[K]V, n)。 - 在已确认的热路径上使用
sync.Pool复用缓冲区——取出时重置, 放回时限制大小(并发技能中有相关模式)。 - 留意隐藏分配:存入接口的值、捕获变量的闭包、
[]byte(s)/string(b)转换,以及任何被go build -gcflags=-m报告为escapes to heap的内容。
2. 热路径优化
- 紧密循环中使用具体类型——接口方法调用会阻碍内联,装箱也会产生分配。
- 热路径上不要使用
reflect;改为生成代码(stringer、protobuf)。 - 连续存储的数据优于指针追逐:使用
[]Point而不是[]*Point;如果循环只访问多条记录中的一个字段,则使用切片组成的结构体。
3. 映射性能
- 已知大小时提供容量提示。
sync.Map只用于其两种有文档说明的工作负载;其他情况使用由互斥锁保护的映射。- 对于较小且固定的键集合(按枚举索引),使用数组或切片更快。
第 1 至 3 节的示例见:references/memory-and-io.md。
4. 基准测试
func BenchmarkParse(b *testing.B) {
input := makeInput(4 << 10) // setup runs once, outside the timed loop
b.ReportAllocs()
for b.Loop() { // Go 1.24+: no ResetTimer, no sink variable
Parse(input)
}
}
- 使用
-count=10和benchstat,不要只运行一次。输出中的~表示“没有变化”,无论百分比显示什么。 - 按输入大小设置子基准;输入大小应贴近生产环境。
- Go 1.24 之前,使用
for range b.N,并在设置完成后调用b.ResetTimer(),同时使用包级 sink。 - 多个竞争变体会并行构建,但基准测试一次只运行一个——并发运行会相互干扰。
命令、阅读指南和 CI 回归检查:references/benchmarking.md。
5. 性能分析
| 症状 | 首选性能分析 |
|---|---|
| CPU 使用率高 | CPU 性能分析——flat 函数占比高 |
| GC 忙碌,分配率高 | 使用 -sample_index=alloc_objects 分析堆 |
| 内存随时间增长 | 获取两份堆快照,使用 go tool pprof -base |
| 延迟高,CPU 空闲 | goroutine 转储、阻塞/互斥锁性能分析、跟踪 |
| 锁竞争 | 互斥锁性能分析(runtime.SetMutexProfileFraction) |
| 调度延迟、GC 暂停 | 执行跟踪;在生产环境使用飞行记录器(Go 1.25+) |
go test -run '^$' -bench Name -cpuprofile cpu.out -memprofile mem.out ./pkg
go tool pprof -http=:0 cpu.out # flame graph
go build -gcflags='-m' ./pkg 2>&1 | grep 'escapes' # what moves to the heap
CPU 性能分析只能观察正在运行的 goroutine:延迟高而 CPU 使用率低时,时间消耗在等待上,只有 goroutine、阻塞和跟踪视图才能显示这一点。命令、阅读指南、逃逸分析和飞行记录器:references/profiling.md。
导入 net/http/pprof 会在 http.DefaultServeMux 上注册处理程序。如果服务也通过 DefaultServeMux 提供公共流量,性能分析数据、命令行和 goroutine 堆栈就会对所有人公开。务必为 pprof 配置独立的回环或内部网络监听器。
6. 运行时调优
- 在容器中,将
GOMEMLIMIT设为内存限制的约 80-90%,并保留GOGC;这样 GC 会在接近上限时更加积极地工作,而不是让进程因 OOM 被终止。仅设置内存限制时,绝不要使用GOGC=off。 - Go 1.25+ 会根据 cgroup CPU 限制推导
GOMAXPROCS——移除automaxprocs,没有测量依据时不要手动设置。 - 将生产环境的 CPU 性能分析数据提交为
default.pgo,用于 PGO(Go 1.21+)。 - 升级工具链后(Go 1.26 的 Green Tea GC、Go 1.27 的分配器),应重新测量,再决定是否保留旧调优。
详情、gctrace 阅读指南和 runtime/metrics:references/runtime-tuning.md。
7. I/O 和 HTTP 客户端
- 每个进程使用一个
http.Client,设置Timeout,并使用从http.DefaultTransport克隆的传输配置,同时调高MaxIdleConnsPerHost——默认值 2 在并发情况下会导致连接频繁更换。 - 关闭每个响应体,并读取到 EOF,以便连接返回连接池。
- 使用流式处理:在设置大小上限的情况下从
r.Body解码,而不是使用io.ReadAll;代理转发使用io.Copy;多次小型写入使用bufio。 - 批量处理往返请求——行、API 调用、通道项目。
- 大缓冲区的子切片和子字符串会使整个缓冲区保持存活;将需要保留的内容通过
slices.Clone/strings.Clone复制。
示例、结构体布局和伪共享:references/memory-and-io.md。
8. 高吞吐日志记录
默认使用 log/slog。只有基准测试显示日志记录出现在性能分析结果中时才切换,并继续使用 slog API 搭配更快的后端(zap 的 zapslog 处理程序),这样调用方无需更改。在热点循环中,每处理 N 个项目记录一次进度,或在循环结束后记录摘要——绝不要逐项记录。
决策表和示例:references/logging.md。
9. 常见反模式
| 反模式 | 修复方法 |
|---|---|
用 fmt.Sprintf 将简单的 int 转为 string | strconv.Itoa |
| 在循环中拼接字符串 | strings.Builder |
| 创建切片时不预分配容量 | make([]T, 0, n) |
| 创建 map 时不指定容量 | make(map[K]V, n) |
在函数内调用 regexp.Compile | 在包级别编译一次 |
在热点路径中调用 json.Marshal | 先进行性能分析:从 Go 1.27 起,encoding/json 使用更快的 v2 引擎;只有 JSON 仍然占据主要开销时,才使用代码生成(easyjson) |
| 在紧密循环中记录日志 | 批量记录或抽样记录 |
在非常紧密的内层循环中使用 defer | 手动清理(较少见,先进行基准测试) |
在生产环境中使用 http.Get / 默认客户端 | 使用带有 Timeout 和经过调优的传输配置的共享客户端 |
对请求或响应正文调用 io.ReadAll | 使用解码器流式读取,并限制大小 |
在测试之外使用 reflect.DeepEqual | 使用 slices.Equal、maps.Equal、bytes.Equal |
将 panic/recover 用作控制流 | 返回错误——panic 会捕获堆栈跟踪 |
重要提示
大多数 Go 代码都不受性能限制。可读性和正确性始终优先于微优化。只有在以下情况下才采用这些模式:
- 基准测试证明这条代码路径是瓶颈
- 优化效果显著(提升 >10%)
- 优化后的代码仍然易读且易于维护
过早优化仍然是万恶之源,即使是在 Go 中也是如此。