go-concurrency-review について
Go の並行コードの実装、レビュー、問題調査を支援するスキルです。ライフサイクル、同期、チャネル、コンテキストの指針と詳細な参照資料を備えています。
主な機能
- 01実装・差分レビュー・リーク調査モード
- 02ゴルーチンのライフサイクルと終了経路の確認
- 03チャネル、Mutex、アトミックの指針
- 04コンテキスト伝播と共有状態の確認
- 05レース検出器によるテストの必須化
活用シーン
- 01Go の変更差分にある並行処理のリスク確認
- 02ゴルーチンリークやデータ競合の調査
- 03チャネルの所有者と終了処理の監査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-concurrency-reviewインストール後、Claude Code で/go-concurrency-reviewを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/channels.mdgoroutine-patterns.mdmutex-and-atomics.md詳細
Go 並行処理レビュー
Go の並行処理は強力ですが、見かけによらず簡単に誤りが生じます。 これらのパターンは、ゴルーチンのリーク、データ競合、デッドロックを防ぎます。
詳細なリファレンス資料は必要に応じて読み込みます。
references/channels.md— バッファーサイズ、シグナリング、プロデューサーの終了。references/mutex-and-atomics.md— ミューテックスの配置、ロック範囲、アトミック、sync.Once、RWMutex の落とし穴、sync.Map、sync.Pool、singleflight。references/goroutine-patterns.md— ゴルーチン境界でのパニック回復、パイプライン、 上限付きファンアウト、タイマー。
以下のセクションだけでは不十分な場合に限り、リファレンスファイルを読んでください。
運用モード
開始前に、依頼に合ったモードを選択してください。
- 実装 — 新しい並行処理コードを記述します。以下のパターンを実装ルールとして従ってください。
- 差分レビュー(デフォルト)— 変更されたコードを各セクションに照らして確認し、新しい
go文と共有状態に特に注意します。 - リーク/競合の調査 — 症状(増加するゴルーチン数、
-raceのレポート、デッドロック)がすでに確認されています。「大規模コードベースの監査」と競合検出のセクションから始めて、原因箇所を特定してください。Go 1.27 以降では、まずgoroutineleakプロファイルを読みます。これは、決して解除されない条件でブロックしているゴルーチンを一覧表示します。手順は go-troubleshooting スキルに従ってください。
大規模コードベースの監査
並行処理を全面的に監査する場合は、1 回の通読ではなく、互いに独立した以下のパスを実行してください。
- ゴルーチンのライフサイクル: すべての
go文を見つけ (grep -rn "go func\|go [a-zA-Z]" --include="*.go")、それぞれに終了経路 (コンテキスト、クローズされたチャネル、WaitGroup)があることを確認します。 - 共有状態: 複数のゴルーチンからアクセスされるパッケージレベルの変数と構造体のフィールドを見つけ、ミューテックス/アトミックで保護されていることを確認します。
- チャネルの構成: チャネルごとにプロデューサー/コンシューマーを整理し、一度だけクローズされることと、クローズ済みチャネルへの送信経路がないことを確認します。
- コンテキストの伝播: ブロッキング呼び出しが
context.Contextを受け取り、それを尊重することを確認します。
環境が並列サブエージェントやタスクへの作業委任をサポートしている場合は、各パスをそれぞれに割り当ててください。そうでない場合は、順番に実行します。調査結果には必ず file.go:line を記載してください。最後に必ず go test -race ./... を実行してください。
プリミティブの選択
| 必要なこと | 使用するもの | 理由 |
|---|---|---|
| あるゴルーチンから別のゴルーチンへデータを渡す | チャネル | 送信によって所有権が移ります |
| 共有構造体のフィールドを保護する | sync.Mutex / RWMutex | クリティカルセクションを短く保ち、所有者を明確にできます |
| カウンターまたはフラグ | atomic.Int64、atomic.Bool | ロック不要で、読み取りが途中で分断されません |
| 一度計算し、何度も読み取る | sync.OnceValue | 競合中でも厳密に一度だけ実行されます |
| 同時に行われる同一の呼び出しをまとめる | singleflight.Group | キーごとにバックエンド呼び出しを 1 回だけ行い、N 回にはなりません |
| ホットパスで短命なバッファーを再利用する | sync.Pool | 割り当てを減らせます — ただし、プロファイルを確認した後に限ります |
| 待機対象 | 使用するもの |
|---|---|
| 結果が失敗しない | sync.WaitGroup(wg.Go、Go 1.25+) |
| 最初のエラーが重要 | errgroup.Group |
| 最初のエラーで残りをキャンセルする | errgroup.WithContext |
| 同時に実行する数に上限を設ける | errgroup + SetLimit(n) — 独自実装のプールは使いません |
1. ゴルーチンのライフサイクル管理
すべてのゴルーチンには、明確な終了経路が必須です。起動したまま放置してはいけません。
ゴルーチンを協調動作させるには errgroup を使用します。
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch data: %w", err)
}
長時間実行するゴルーチンではコンテキストを尊重します。
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
if err := w.process(job); err != nil {
w.logger.Error("process job", slog.Any("error", err))
}
}
}
}
ゴルーチンは呼び出し先ではなく、所有者側で起動します。
// ✅ Good — caller controls lifecycle
go worker.Run(ctx)
// ❌ Bad — function secretly starts goroutine
func NewWorker() *Worker {
w := &Worker{}
go w.run() // hidden goroutine — caller has no control
return w
}
ゴルーチンの境界でパニックを回復します
いずれかのゴルーチンでパニックが発生すると、プロセス全体が終了します。HTTP ミドルウェア内の recover が対象にするのはリクエストのゴルーチンだけであり、そこから起動されたゴルーチンは対象になりません。入力依存のコードやサードパーティーのコードを実行するワーカーやバックグラウンドタスクでは、それぞれの遅延実行関数内でパニックを回復し、スタックをログに記録して、パニックをエラーに変換します。自分が所有するコードのバグを隠すために回復してはいけません。無関係な処理まで巻き込んでクラッシュするおそれがある箇所で回復してください。例:references/goroutine-patterns.md。
2. チャネルのパターン
-
サイズは 1 以下にします。バッファなしは同期ポイントであり、バッファサイズ 1 は受け渡しです。それより大きなバッファには、数値の根拠を説明するコメントが必要です。任意の
100は、本番環境がステージング環境より遅くなる日を待つバグです。 -
シグナルチャネルには
struct{}を使用し、close(done)ですべての受信側に一斉に通知します。 -
チャネルの所有者はプロデューサーであり、生成するゴルーチン内の
defer close(ch)で、プロデューサーだけがチャネルを閉じます。すべての送信はctx.Done()とのselectに入れます。そうしないと、コンシューマーが途中で処理をやめたときにプロデューサーがリークします。 -
パイプラインでは、各ステージが自身の出力を閉じます。ファンアウトは
errgroup.SetLimitで制限します。単一ゴルーチンの連鎖にはチャネルではなくiter.Seqを使います。
例は references/channels.md と references/goroutine-patterns.md にあります。
3. Mutex とアトミック操作
- ゼロ値の
sync.Mutexとsync.RWMutexはそのまま使用できます。*sync.Mutex型のフィールドは常に誤りです。 - Mutex は、保護するフィールドのすぐ上に宣言し、その関連性を説明するコメントを付けます。「構造体全体」を保護する Mutex では、具体的に何も保護できません。
- クリティカルセクションは最小限にします。ロックを保持したまま外部サービスを呼び出したり、別のロックを取得したりしてはいけません。
- 🔴 Mutex を含む値をコピーしてはいけません(
c2 := *c1)。コピーには元のロック状態が引き継がれます。go vetはこのようなケースの大半を検出します。信頼してください。 int64を Mutex で保護する代わりに、カウンターやフラグにはsync/atomic型を使用します。- 必ず一度だけ実行する必要がある遅延初期化には
sync.Onceを使います。 RWMutexは昇格できず、再帰的な読み取りロックもできません。sync.Mapが適するのは 2 種類のワークロードだけです。sync.Poolのオブジェクトはリセットし、サイズに上限を設ける必要があります。singleflightは最初の呼び出し元のコンテキストで実行されるため、切り離してください。詳細はreferences/mutex-and-atomics.mdを参照してください。
例は references/mutex-and-atomics.md にあります。
4. コンテキストの伝播
コンテキストは常に最初のパラメーターにし、構造体のフィールドにはしません。また、すべてのブロッキング処理で ctx.Done() を使った select を行います。
// ✅ Good
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ Bad — blocks forever if the context is cancelled
result := <-ch
外部呼び出しごとに、独自のタイムアウトを設定した子コンテキストを作成し、すぐに defer cancel() を呼び出します。ルールの詳細は go-context スキルを参照してください。
5. 可変グローバルを避ける
// ❌ Bad — mutable global, not safe for concurrent access
var db *sql.DB
// ✅ Good — pass as dependency
type Server struct {
db *sql.DB
}
レース検出
CI では、必ずレース検出器を使ってテストを実行します。
go test -race ./...
これは絶対条件です。-race なしでテストスイートが成功しても、並行処理の正しさは何も証明されません。
レッドフラグのチェックリスト
- 🔴 シャットダウン手段のないゴルーチンの起動
- 🔴 チャネルが閉じられない(ゴルーチンがリークする可能性)
- 🔴 Mutex を値としてコピー
- 🔴 コンテキストを構造体のフィールドに格納
- 🔴 親コンテキストを利用できる箇所で
context.Background()を使用 - 🔴 ブロッキング処理の
selectにctx.Done()のケースがない - 🔴 共有マップ/スライスに同期なしでアクセス
- 🟡 任意に大きなサイズを指定したバッファ付きチャネル
- 🟡 適切なシグナル通知の代わりに同期目的で
time.Sleepを使用 - 🟡 ライフサイクル制御なしに
init()やコンストラクター内でゴルーチンを起動 - 🟡
recoverなしで、入力に依存するコードを実行するバックグラウンドゴルーチン