
こんにちは。よっしーです(^^)
本日は、Go言語のパフォーマンス分析ついて解説しています。
背景
Go言語でアプリケーションを開発していると、必ずと言っていいほど直面するのがパフォーマンスの問題や予期しないバグです。「なぜこんなにメモリを消費しているのか?」「どこで処理が遅くなっているのか?」「このゴルーチンはなぜデッドロックしているのか?」—— こうした疑問に答えるために、Goのエコシステムには強力な診断ツール群が用意されています。
しかし、これらのツールは種類が多く、それぞれ異なる目的と特性を持っているため、どのような場面でどのツールを使うべきか迷ってしまうことも少なくありません。プロファイリング、トレーシング、デバッギング、ランタイム統計…それぞれのツールが解決できる問題は異なり、時には相互に干渉し合うこともあります。
本記事では、Go公式ドキュメントの診断ツールに関する解説を日本語で紹介しながら、実際の開発現場でどのように活用できるかを探っていきます。適切なツールを選択し、効果的に問題を診断することで、より高品質で高性能なGoアプリケーションの開発が可能になるはずです。特に、パフォーマンスのボトルネックを特定し改善することは、ユーザー体験の向上やインフラコストの削減に直結する重要なスキルと言えるでしょう。
デバッギング
デバッギングは、プログラムが誤動作する理由を特定するプロセスです。デバッガーを使用することで、プログラムの実行フローと現在の状態を理解できます。デバッギングには複数のスタイルがありますが、このセクションではプログラムへのデバッガーのアタッチとコアダンプデバッギングにのみ焦点を当てます。
Goユーザーは主に以下のデバッガーを使用します:
- Delve: DelveはGoプログラミング言語用のデバッガーです。Goのランタイムコンセプトとビルトイン型をサポートしています。Delveは、Goプログラム用の完全な機能を持つ信頼性の高いデバッガーになろうとしています。
- GDB: Goは標準Goコンパイラーとgccgoを通じてGDBサポートを提供します。スタック管理、スレッディング、ランタイムには、GDBが期待する実行モデルとは十分に異なる側面が含まれており、gccgoでプログラムをコンパイルした場合でも、デバッガーを混乱させる可能性があります。GDBはGoプログラムのデバッグに使用できますが、理想的ではなく、混乱を引き起こす可能性があります。
デバッギング – プログラムの「手術」
デバッギングとは?
例え: 医師が患者を診察するように、プログラムの「症状」から「病因」を特定する作業
プログラムが思い通りに動かない時、その原因を突き止めて修正する過程です。
🛠️ デバッガーの基本機能
| 機能 | 説明 | 例え |
|---|---|---|
| ブレークポイント | コードの特定行で実行を停止 | 本の特定ページにしおりを挟む |
| ステップ実行 | 1行ずつコードを実行 | スローモーション再生 |
| 変数の監視 | 変数の値を確認 | 体温計で熱を測る |
| スタックトレース | 関数呼び出しの履歴 | パンくずの道筋 |
| 条件付きブレーク | 条件が満たされた時に停止 | アラーム設定 |
Delve – Go専用の最強デバッガー
インストールと基本的な使い方
# Delveのインストール
go install github.com/go-delve/delve/cmd/dlv@latest
# デバッグの開始
dlv debug main.go
# テストのデバッグ
dlv test
# 実行中のプロセスにアタッチ
dlv attach <pid>
実践例:バグのあるコードをデバッグ
// buggy.go - バグのあるコード
package main
import "fmt"
func calculateTotal(items []int) int {
total := 0
for i := 0; i <= len(items); i++ { // バグ: <= は < であるべき
total += items[i] // パニックが発生する
}
return total
}
func main() {
numbers := []int{10, 20, 30, 40, 50}
result := calculateTotal(numbers)
fmt.Printf("Total: %d\n", result)
}
Delveでのデバッグセッション
$ dlv debug buggy.go
Type 'help' for list of commands.
(dlv) break main.calculateTotal # ブレークポイント設定
Breakpoint 1 set at 0x497380 for main.calculateTotal()
(dlv) run # プログラム実行
> main.calculateTotal() ./buggy.go:5
(dlv) print items # 変数の確認
[]int len: 5, cap: 5, [10,20,30,40,50]
(dlv) next # 次の行へ
> main.calculateTotal() ./buggy.go:6
(dlv) locals # ローカル変数一覧
total = 0
i = 0
(dlv) continue # ブレークポイントまで続行
Delveの高度な機能
1. 条件付きブレークポイント
// 特定の条件でのみ停止
func processUsers(users []User) {
for _, user := range users {
if user.Age > 65 { // ← ここで止めたい
applyDiscount(user)
}
}
}
Delveでの設定:
(dlv) break processUsers
(dlv) condition 1 user.Age > 65 # 条件設定
2. ゴルーチンのデバッグ
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
// デバッグポイント
result := complexCalculation(id)
fmt.Printf("Worker %d result: %d\n", id, result)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
}
Delveでゴルーチンを調査:
(dlv) goroutines # 全ゴルーチン一覧
* Goroutine 1 - main.main
Goroutine 2 - worker (id=0)
Goroutine 3 - worker (id=1)
Goroutine 4 - worker (id=2)
(dlv) goroutine 2 # ゴルーチン2に切り替え
Switched to goroutine 2
(dlv) stack # スタックトレース確認
0: worker() ./main.go:10
1: runtime.goexit()
3. ウォッチポイント(メモリ監視)
type Config struct {
MaxRetries int
Timeout time.Duration
Debug bool // この値が変わる瞬間を捕まえたい
}
func updateConfig(cfg *Config) {
// どこかでcfg.Debugが変更される
if someCondition() {
cfg.Debug = true // ← ここで停止
}
}
VSCode統合デバッグ
launch.json設定
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"env": {
"DEBUG": "true"
},
"args": ["--config", "debug.yaml"],
"showLog": true
},
{
"name": "Attach to Process",
"type": "go",
"request": "attach",
"mode": "local",
"processId": "${command:pickProcess}"
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}",
"args": [
"-test.run",
"TestSpecificFunction"
]
}
]
}
コアダンプデバッギング
コアダンプの生成と分析
package main
import (
"os"
"runtime/debug"
)
func init() {
// パニック時にコアダンプを生成
debug.SetTraceback("crash")
}
func problematicFunction() {
var ptr *int
*ptr = 42 // nil pointer dereference
}
func main() {
// GOTRACEBACK環境変数を設定
os.Setenv("GOTRACEBACK", "crash")
problematicFunction()
}
コアダンプの分析:
# コアダンプファイルが生成される
$ ls core.*
core.12345
# Delveでコアダンプを開く
$ dlv core ./myprogram core.12345
(dlv) bt # バックトレース
0: panic()
1: main.problematicFunction()
2: main.main()
(dlv) frame 1 # フレーム1に移動
(dlv) locals # その時点のローカル変数
デバッグのベストプラクティス
1. ログとデバッガーの使い分け
// デバッグ用ログを仕込む
func processOrder(order Order) error {
log.Printf("Processing order: %+v", order)
// 複雑な処理
if err := validateOrder(order); err != nil {
log.Printf("Validation failed: %v", err)
return err
}
// デバッガーで確認したいポイント
result := calculatePrice(order) // ← ブレークポイント
log.Printf("Calculated price: %v", result)
return nil
}
2. リモートデバッギング
// Dockerコンテナでのデバッグ
// Dockerfile
FROM golang:1.21
RUN go install github.com/go-delve/delve/cmd/dlv@latest
WORKDIR /app
COPY . .
# デバッグモードで起動
CMD ["dlv", "debug", "--headless", "--listen=:2345", "--api-version=2"]
接続方法:
# ローカルから接続
dlv connect localhost:2345
デバッグ戦略チャート
問題の症状
↓
パニック? → Yes → スタックトレース確認
↓ No
無限ループ? → Yes → プロファイリング + ブレークポイント
↓ No
間違った値? → Yes → ステップ実行 + 変数監視
↓ No
並行処理問題? → Yes → ゴルーチン分析 + race detector
↓ No
メモリリーク? → Yes → ヒーププロファイル + GCトレース
デバッグチェックリスト
準備段階:
□ 再現可能な最小限のテストケース作成
□ エラーメッセージとスタックトレースの収集
□ 関連するログの確認
デバッグ実行:
□ 適切なブレークポイントの設定
□ 変数の値の確認
□ 実行フローの追跡
□ ゴルーチンの状態確認
問題解決後:
□ 修正のテスト
□ 同様のバグの可能性を確認
□ ドキュメント化
□ 予防策の検討
トラブルシューティングのコツ
- 「printf デバッグ」も有効
- 時には単純な
fmt.Printfが最速 - 特に並行処理の問題
- 時には単純な
- バイナリサーチ法
- 問題箇所を半分ずつ絞り込む
- コメントアウトで範囲を狭める
- 新鮮な目で見る
- 休憩を取る
- 他の人に説明する(ラバーダック法)
デバッガーは強力なツールですが、問題解決には「仮説→検証→修正」のサイクルを回すことが重要です。Delveを使いこなすことで、このサイクルを効率的に回せるようになります!
おわりに
本日は、Go言語のパフォーマンス分析について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)

コメント