mcpField
G
go-performance-review

go-performance-review

github.com/eduardo-sl开发83187 次安装2 周前更新
代码仓库

关于 go-performance-review

通过分析和测量来调查及改善 Go 性能的技能,指导架构与热点审查、基准测试驱动的改动,以及运行时、内存、I/O 和日志分析。

主要功能

  1. 01架构、热点与优化模式
  2. 02使用 benchstat 对比基准测试
  3. 03按症状选择性能分析方法
  4. 04审查内存、分配与 I/O
  5. 05运行时调优和日志建议

适用场景

  1. 01查找 CPU 或内存占用偏高的原因
  2. 02将优化效果与基线进行比较
  3. 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.md
logging.md
memory-and-io.md
profiling.md
runtime-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。
  • 热路径审查(调用者指定的一个函数):结合其性能分析结果,按顺序阅读。
  • 优化(已知瓶颈):按以下周期执行,每次只做一项改动。

优化周期

  1. 明确指标和目标——p99 延迟、吞吐量、 allocs/op、RSS。没有目标,优化就不会结束。
  2. 先排除外部耗时。 如果 goroutine 停在 net.(*conn).Read 或 database/sql,或者跟踪中有一个上游跨度占据主导地位,就说明应在别处修复(go-database、go-observability)。
  3. 对隔离路径进行基准测试并保存基线: go test -run '^$' -bench Name -benchmem -count=10 | tee old.txt。
  4. 通过性能分析找到原因——见下文“性能分析”。
  5. 只改一项内容,并添加注释说明这样做为何更快。
  6. 在同一台机器和同一工具链上重新测量;只有当 benchstat old.txt new.txt 显示出显著差异时,才保留此改动。
  7. 将 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 转为 stringstrconv.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 代码都不受性能限制。可读性和正确性始终优先于微优化。只有在以下情况下才采用这些模式:

  1. 基准测试证明这条代码路径是瓶颈
  2. 优化效果显著(提升 >10%)
  3. 优化后的代码仍然易读且易于维护

过早优化仍然是万恶之源,即使是在 Go 中也是如此。

评论

支持 Markdown