go-performance-review について
プロファイリングと計測を通じて Go の性能を調査・改善するスキルです。アーキテクチャやホットパスのレビュー、ベンチマークに基づく変更、ランタイム、メモリ、I/O、ログの分析を支援します。
主な機能
- 01アーキテクチャ・ホットパス・最適化モード
- 02benchstat によるベンチマーク比較
- 03症状に応じたプロファイリング指針
- 04メモリ、割り当て、I/O のレビュー
- 05ランタイム調整とログの助言
活用シーン
- 01CPU やメモリ使用量が高い原因の特定
- 02最適化の効果をベースラインと比較
- 03HTTP、割り当て、ログのコストのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
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、 ホットパスにおけるロギングのアンチパターン。
以下の概要だけでは不十分な場合に限り、参考ファイルを読んでください。
運用モード
- アーキテクチャレビュー(パッケージまたはサービス):構造的な コストを探します。たとえば、接続を再利用しない、上限のないゴルーチン、N+1 クエリ、 不適切なデータ構造、一度だけ実行すればよいのにリクエストごとに実行している処理です。 アロケーションとレイアウト、I/O と並行処理、アルゴリズムとキャッシュについて 別々にレビューします。環境が対応している場合は、それぞれを並列のサブエージェントに割り当ててください。
- ホットパスレビュー(呼び出し元が指定した単一の関数):プロファイルとともに、 順に読みます。
- 最適化(ボトルネックが判明している場合):以下のサイクルに従い、一度に 1 つずつ変更します。
最適化サイクル
- メトリクスと目標を明確にします — p99 レイテンシ、スループット、
allocs/op、RSS。目標がなければ、最適化に終わりはありません。 - まず外部処理にかかる時間を除外します。 ゴルーチンが
net.(*conn).Readまたはdatabase/sqlで待機している場合や、トレースの大半が 1 つの 上流スパンで占められている場合は、修正すべき場所は別にあります(go-database、go-observability)。 - 対象の処理を切り出してベンチマークし、ベースラインを保存します:
go test -run '^$' -bench Name -benchmem -count=10 | tee old.txt。 - プロファイルを取得して原因を特定します — 下記の「プロファイリング」を参照してください。
- 変更は 1 つに絞り、速くなる理由をコメントに記載します。
- 同じマシンとツールチェーンで再測定し、
benchstat old.txt new.txtで有意な差が示された場合のみ変更を残します。 - benchstat の表をコミット本文に記載します(
perf(scope): ...)。
1. アロケーションの削減
- 単一の値には
fmt.Sprintfではなくstrconvを使います。 - ループ内での
+=ではなく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を使い、 多数のレコードの 1 つのフィールドをループで処理する場合はスライスの構造体を使います。
3. マップのパフォーマンス
- サイズが分かっている場合は容量のヒントを指定します。
sync.Mapは、文書化されている 2 つの用途に限って使います。それ以外の場合は、ミューテックスで保護したマップを使います。- キーの種類が少なく固定されている場合(列挙型でインデックス化できる場合)は、配列またはスライスのほうが高速です。
セクション 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 未満では、セットアップ後に
b.ResetTimer()を呼び出し、パッケージレベルのシンクとともにfor range b.Nを使用してください。 - 競合する各バリアントは並列にビルドしますが、ベンチマークは一度に 1 つずつ実行してください。同時実行すると互いに影響します。
コマンド、読み方のガイド、CI でのリグレッションチェックについては、references/benchmarking.md を参照してください。
5. プロファイリング
| 症状 | 最初に取得するプロファイル |
|---|---|
| CPU 使用率が高い | CPU プロファイル — flat が高い関数 |
| GC が頻繁に動作し、割り当て率が高い | -sample_index=alloc_objects を指定したヒーププロファイル |
| メモリ使用量が時間とともに増加する | 2 つのヒープスナップショット、go tool pprof -base |
| レイテンシーが高く、CPU 使用率が低い | goroutine ダンプ、block/mutex プロファイル、トレース |
| ロック競合 | mutex プロファイル(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、block、trace の各ビューだけです。コマンド、読み方のガイド、エスケープ解析、フライトレコーダーについては、references/profiling.md を参照してください。
net/http/pprof をインポートすると、ハンドラーが http.DefaultServeMux に登録されます。サービスが DefaultServeMux から一般公開のトラフィックも配信している場合、プロファイル、コマンドライン、goroutine のスタックが誰でも閲覧できる状態になります。pprof には必ず、専用のループバックまたは内部ネットワークのリスナーを使用してください。
6. ランタイムのチューニング
- コンテナーでは、
GOMEMLIMITをメモリー上限の約 80〜90% に設定し、GOGCはそのままにしてください。上限に近づくと GC がより頻繁に動作し、プロセスが OOM キルされるのを防ぎます。上限だけを設定してGOGC=offにするのは避けてください。 - Go 1.25 以降では
GOMAXPROCSが cgroup の CPU 上限から設定されるため、automaxprocsを削除し、測定なしに手動で設定しないでください。 - PGO(Go 1.21 以降)用に、本番環境の CPU プロファイルを
default.pgoとしてコミットしてください。 - ツールチェーンをアップグレードした後(Go 1.26 の Green Tea GC、Go 1.27 のアロケーター)は、以前のチューニングを維持する前に再測定してください。
詳細、gctrace の読み方、runtime/metrics については、references/runtime-tuning.md を参照してください。
7. I/O と HTTP クライアント
- プロセスごとに
http.Clientを 1 つ使用し、Timeoutを設定してください。また、http.DefaultTransportを複製してMaxIdleConnsPerHostを増やしたトランスポートを使用してください。デフォルト値の 2 では、並行処理時に接続が頻繁に作り直されます。 - すべてのレスポンスボディを閉じ、EOF まで読み取って、接続がプールに戻るようにしてください。
- ストリーミングする場合は、
io.ReadAllの代わりにサイズ上限を設けてr.Bodyからデコードしてください。プロキシにはio.Copyを、多数の小さな書き込みにはbufioを使用してください。 - ラウンドトリップをまとめてください(行、API 呼び出し、チャネル項目など)。
- 大きなバッファーのサブスライスや部分文字列は、バッファー全体をメモリー上に保持します。保持するデータには
slices.Clone/strings.Cloneを使用してください。
例、構造体のレイアウト、偽共有については、references/memory-and-io.md を参照してください。
8. 高スループットのログ記録
デフォルトは log/slog です。ベンチマークでログ記録がプロファイル上のボトルネックだと確認された場合に限り切り替えてください。その場合も、呼び出し側を変更せずに済むよう、より高速なバックエンド(zap の zapslog ハンドラー)を使って slog API を維持してください。ホットループでは、N 件ごとに進捗をログに記録するか、ループ終了後に概要を記録してください。項目ごとに記録するのは避けてください。
判断表と例については、references/logging.md を参照してください。
9. よくあるアンチパターン
| アンチパターン | 修正方法 |
|---|---|
単純な int→string 変換に fmt.Sprintf を使う | strconv.Itoa を使う |
| ループ内で文字列を連結する | strings.Builder を使う |
| スライスを事前確保せずに使う | make([]T, 0, n) を使う |
| 容量を指定せずにマップを使う | 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 を使う | エラーを返します。パニックが発生するとスタックトレースが記録されます |
重要な注意事項
Go のコードのほとんどは、パフォーマンスが重要ではありません。マイクロ最適化よりも、読みやすさと正確さを常に優先してください。次の条件を満たす場合にのみ、これらのパターンを適用してください。
- ベンチマークで、このコードパスがボトルネックだと確認できる
- 最適化による効果が大きい(10%を超える改善)
- 最適化後のコードも、読みやすく保守しやすい
早すぎる最適化は、Go でも依然として諸悪の根源です。