mcpField
G
go-troubleshooting

go-troubleshooting

github.com/eduardo-sl開発83101 回インストール2 週間前に更新
リポジトリ

go-troubleshooting について

Go 開発者向けのトラブルシューティングスキルです。変更前に問題を再現して証拠を集め、根本原因を特定する手順を重視します。クラッシュ、デッドロック、リーク、競合、テスト、ビルドの症状別手順と参考資料を提供します。

主な機能

  1. 01問題の再現と証拠の収集を優先
  2. 02症状に応じた診断手順
  3. 03pprof によるメモリとゴルーチンの調査
  4. 04競合、フレークテスト、ビルドエラーの調査
  5. 05回帰テストによる修正の検証

活用シーン

  1. 01panic のスタックから Go の問題を追跡
  2. 02停止、デッドロック、リソースリークの調査
  3. 03断続的なテスト失敗の再現と診断
  4. 04Go モジュールやツールチェーンのビルドエラー対応

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-troubleshooting

インストール後、Claude Code で/go-troubleshootingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
common-bugs.md
flaky-and-build.md
methodology.md

詳細

Go のトラブルシューティング

修正の前に原因を診断します。再現し、観察し、原因箇所を特定してから、コードを変更します。 説明できていない症状を決して「修正」しないでください。バグが別の場所に移るだけです。

必要に応じて読み込む詳細な参考資料:

  • references/methodology.md — デバッグの手順、コードを責める前に呼び出し元を追跡する方法、git bisect、自分の推論に潜む危険信号。
  • references/flaky-and-build.md — 不安定なテストの再現方法とよくある原因、ビルド、モジュール、ツールチェーンのエラーとその修正方法。
  • references/common-bugs.md — コンパイルは正常に通るのに誤動作する Go のバグについて、症状 → バグ → 修正をまとめた表。

以下の概要で不十分な場合に限り、参考ファイルを読んでください。

0. 方法

  1. 期待される結果と実際の結果を明確にし、証拠をすべて収集します(スタック全体、正確な入力、トレース ID)。
  2. 失敗するテストで再現し、その後、再現ケースを最小化します。
  3. 仮説を1つ立て、変更を1つ加え、観察を1回行います。変更を重ねてはいけません。
  4. きっかけではなく原因を修正します。誤った値が存在した理由を問い続けてください。
  5. 修正に3回失敗したら、メンタルモデルが間違っています。証拠をもとにやり直してください。原因が自分より前から存在する場合は、git bisect run で該当コミットを見つけます。

1. 症状に応じて手順を選ぶ

症状手順
クラッシュはしないが、結果が間違っている§0 の方法に従い、その後 references/common-bugs.md を確認
スタックトレースを伴うクラッシュ§2 パニックを読む
プログラムが停止する/リクエストが滞る§3 ゴルーチンをダンプし、ブロック箇所を特定
fatal error: all goroutines are asleep§3 — Go が全体的なデッドロックを検出
メモリが増え続け、OOM に至る§4 ヒーププロファイルの差分を確認
ゴルーチン数が増え続ける§5 ゴルーチンプロファイルの差分を確認
データが断続的に破損する/値がおかしい§6 レースディテクター
状態を対話的に調べる必要がある§7 Delve
現在、本番環境でインシデントが発生している§11 再起動の前に記録を取得
テストが断続的に失敗する§9 不安定なテスト
ビルド、モジュール、ツールチェーンのエラー§10 ビルドの失敗

2. パニックの読み方

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6bb0e4]

goroutine 43 [running]:
myapp/internal/service.(*UserService).Notify(0x0, {0xc000123456?, ...})
        /app/internal/service/user.go:87 +0x24
myapp/internal/handler.(*Handler).Create(0xc0001a2000, ...)
        /app/internal/handler/user.go:41 +0x1c5

機械的に読みます。

  1. 1行目:パニックの種類。nil pointer dereference と addr=0x0 の組み合わせは、nil のレシーバー、nil のフィールド、または nil のマップ/ポインター引数を意味します。
  2. 自分のコードにある最上位のフレーム:user.go:87 — その箇所に移動します。
  3. フレーム内のレシーバー値:(*UserService).Notify(0x0, ...) — 最初の引数 0x0 はレシーバーそのものです。つまり、サービス自体が nil でした。構築された場所(または構築されなかった理由)を追跡します。
  4. goroutine 43 — ゴルーチン1以外の場合は、何がそれを起動したのか、その場所にリカバリー境界があるべきかを調べます。

3. ハングとデッドロック

ハングしているプロセスからゴルーチンのダンプを取得します。

kill -QUIT <pid>      # dumps all goroutine stacks to stderr, then exits
# or, if net/http/pprof is mounted:
curl 'localhost:6060/debug/pprof/goroutine?debug=2'

次に、スタックを分類します。

  • sync.(*Mutex).Lock にある [semacquire] — ミューテックスを保持しているゴルーチンを探します。クリティカルセクション内にある別のスタックを確認してください。2つのゴルーチンがそれぞれ2つのロックのどちらかを保持している場合は、ロック順序が逆転しています。
  • [chan send]/[chan receive] — 相手側がいなくなっています。受信/送信するはずのゴルーチンと、それが終了した理由(または、そもそも起動されていない理由)を探します。
  • ctx.Done() のケースがない [select] — キャンセルを無視してブロックしている呼び出しです。
  • 同一のスタックが数百個ある — デッドロックではなく、リークです(§5)。

停止しているのではなく、処理が遅い場合は、runtime.SetMutexProfileFraction/SetBlockProfileRate を有効にしてミューテックスプロファイルとブロックプロファイルを確認するか、5秒間のトレース(/debug/pprof/trace?seconds=5)を取得して、実行可能状態なのに実行されていないゴルーチンを確認します。kill -QUIT を実行するとプロセスが終了します。本番環境では、pprof エンドポイントを使用してください。

4. メモリーリーク

長時間稼働するサービスでは、net/http/pprof をプライベートなリスナー(localhost:6060)でマウントし、公開 mux では決してマウントしないでください。

時間をおいてヒーププロファイルを比較します。リークとは、増加したメモリが減らない状態です。

curl -s localhost:6060/debug/pprof/heap > heap1.pb.gz
sleep 300   # let the leak accumulate
curl -s localhost:6060/debug/pprof/heap > heap2.pb.gz
go tool pprof -base heap1.pb.gz heap2.pb.gz
(pprof) top          # biggest positive delta = the leak
(pprof) list FuncName

よくある原因:追い出し処理のない無制限のキャッシュ/マップ、大きな配列を保持し続けるサブスライス、停止されない time.Ticker、クローズされないレスポンスボディ、増え続けるグローバルスライス、バッファーを保持し続ける放置されたゴルーチン。

5. Goroutine のリーク

Go 1.27 以降では、リークプロファイルから始めます。解放する手段がないチャネルやロックでブロックされている goroutine を確認します。

go tool pprof http://localhost:6060/debug/pprof/goroutineleak

グローバル変数によって生存し続けている goroutine や、まだ実行中の goroutine は確認できません。その場合は、2 つの goroutine プロファイルを比較します。

curl -s localhost:6060/debug/pprof/goroutine > g1.pb.gz
sleep 300
curl -s localhost:6060/debug/pprof/goroutine > g2.pb.gz
go tool pprof -base g1.pb.gz g2.pb.gz
(pprof) top    # the growing stack is your leak site

リークしているスタックから、終了しない go ステートメントが分かります。コンテキストやチャネルのクローズなど、終了までの経路を修正します。パターンについては concurrency スキルを参照してください。テストでは、goleak(uber-go/goleak)を使うと、goroutine を残したままのテストを失敗させられます。

6. Race Detector

go test -race ./...        # in CI, always
go build -race ./cmd/api   # staging binaries under real traffic

レポートには、書き込みと、それと同時に行われた読み取りまたは書き込みの 2 つのスタックが表示され、それぞれに goroutine の生成箇所も示されます。修正方法として「スリープを追加する」ことはありません。状態を保護する(ミューテックス)、所有権を移す(チャネル)、または不変にします。-race が報告するのは実際に実行されたデータ競合のみです。テストしていない経路がある場合、問題が検出されなかったとしても何も証明されません。

7. Delve

dlv test ./internal/service -- -test.run TestTransfer   # debug a test
dlv attach <pid>                                        # running process
dlv core ./api core.1234                                # post-mortem

(dlv) break user.go:87
(dlv) continue
(dlv) print svc.repo          # inspect exact values
(dlv) goroutines -t           # all goroutines with stacks
(dlv) goroutine 43 bt         # switch and backtrace

場所だけでなく、実際の値や goroutine の状態を確認する必要がある場合は delve を使います。手早く場所を特定するには、対象を絞った t.Logf または slog.Debug を追加してテストを 1 回実行する方が、多くの場合は速く済みます。

8. 診断用環境変数

GOTRACEBACK=all ./api        # panic dumps ALL goroutines, not just one
GODEBUG=gctrace=1 ./api      # GC cycles: pacing, heap goal, pause times
GOMEMLIMIT=512MiB ./api      # soft memory limit — mitigates OOM while
                             # you find the real leak
GOTRACEBACK=crash ./api      # also write a core dump, for `dlv core`
GODEBUG=schedtrace=1000 ./api  # scheduler state every second

Go 1.27 以降では、トレースバックに pprof の goroutine ラベルが出力されます。ラベルに機密情報が含まれる場合は、GODEBUG=tracebacklabels=0 を設定してください。

9. 不安定なテスト

手を加える前に、まず任意のタイミングで失敗させます。-count=200 -failfast、続いて -race、-shuffle=on(出力されたシードで再実行)、そして -cpu=1,2,8 を試します。よくある原因は、同期のために使われるスリープ、実際の時計、共有されたパッケージ状態、アサーションでのマップの順序、固定ポート、以前のテストから残った goroutine です。CI で再試行しても、バグがユーザーに見つからなくなるわけではなく、あなたに見えなくなるだけです。

10. ビルドエラー

最初のエラーだけを確認してください。後続のエラーは通常、その結果として発生しています。ツールチェーンが古い(GOTOOLCHAIN)、go.sum のエントリーが不足している、v1 と v2 のモジュールが同時に存在するためインポートが曖昧になっている、OS 固有のファイル、C コンパイラーがない、プライベートモジュール(GOPRIVATE)、Go 1.27 の stdversion チェックには、それぞれ決まった対処法があります。

コマンドと原因 → 修正の一覧表については、references/flaky-and-build.md を参照してください。

11. 本番環境のインシデント

再起動する前に取得してください。再起動すると証拠が失われます。

  1. goroutine ダンプ(/debug/pprof/goroutine?debug=2)、ヒーププロファイル、30 秒間の CPU プロファイル、5 秒間のトレースを取得します。または、サービスで使用している場合はフライトレコーダーのスナップショット(Go 1.25 以降)を取得します。
  2. 正確なビルドを確認します。go version -m ./bin/api で、Go のバージョン、モジュールのバージョン、VCS リビジョンが表示されます。
  3. その後に再起動し、上記の手順で取得したデータをオフラインで分析します。pprof は内部ポートのみで公開してください。スタックとコマンドラインが公開されるためです。

検証チェックリスト

  1. コードを変更する前に、症状を再現する(またはダンプ/プロファイルで取得する)
  2. 根本原因を説明できる。その箇所で失敗が起きた理由を説明できる
  3. panic の修正では、ラッパーの recover ではなく nil/範囲外の原因に対処する
  4. デッドロックの修正では、ロック順序を一意に定めるか、共有ロックを取り除く
  5. リークの修正を検証する。修正後に goroutine/ヒーププロファイルが横ばいになっている
  6. 並行処理に関する修正の後に go test -race ./... が成功する
  7. 修正がなければ失敗するリグレッションテストを追加する
  8. pprof エンドポイントは localhost/プライベートインターフェースのみにバインドする

コメント