はじめに
goroutine は軽いですが、リークすると確実にサービスを蝕みます。
- メモリがじわじわ増える
- CPUが上がる
- タイムアウトが増える
- 再起動でしか直らない
多くの原因は「終了条件が設計されていない」ことです。
この記事は、終了条件の型と、レビューで落とせる観点をチェックリストとしてまとめます。
先に結論 起動したら停止もセット
- goroutine を起動する場所を限定する
- 止め方を決める
- context.Done
- channel close
- 明示フラグ
- 待ち合わせを決める
- WaitGroup
- errgroup
リークしやすいパターン
無限ループ + break条件なし
- select の default で回り続ける
- 外部APIが止まったときに永遠に待つ
受信側がいない channel 送信
- バッファなし channel で詰まる
- 例外経路で受信が消えて詰まる
context を渡していない
- HTTPリクエストが切れても下流が止まらない
- タイムアウトになっても処理が残る
実装テンプレ(最小)
context で止める
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
_ = job
}
}
}
errgroup で待つ
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return runA(ctx)
})
g.Go(func() error {
return runB(ctx)
})
if err := g.Wait(); err != nil {
// どれかが失敗
}
監視と調査の観点
- goroutine数をメトリクス化する
- タイムアウト率やキャンセル率を追う
- pprof で goroutine dump を取れるようにしておく
「増えて戻らない」ならリークの可能性が高いです。
レビュー用チェックリスト
- goroutine の停止条件がコードとして存在する
- context が境界から渡されている
- channel close の責務が明確
- 送信がブロックしうる経路で、受信が必ず存在する
- 異常系でも WaitGroup や errgroup が必ず完了する