goroutine設計レビュー完全ガイド:競合・リーク・設計崩壊を読み解く
goroutine設計レビュー完全ガイド:競合・リーク・設計崩壊を読み解く
Go言語における goroutine は極めて強力な並行処理機構である。
「簡単に並列化できる」という利便性ゆえ、レビューアーにとっては危険信号を読み解く目が問われる領域でもある。
goroutineとは
goroutineはGoにおける軽量スレッド。goキーワードを付けて関数やクロージャを起動するだけで並行処理が開始される。
しかし、終了待機・エラー伝播・キャンセル制御は自動では行われず、実装者が全て設計しなければならない。
本記事では以下の構成でレビューアーの実務判断スキル養成を目的に整理していく。
- 良い実装例の技術背景解説(指摘コメントなし)
 - 問題のある実装例とレビュー指摘
 - チェックリスト
 - ケーススタディ
 - 最後にレビュー観点まとめ
 
1. 正しいgoroutine設計例とその設計意図
まずは良い実装例から、どのような観点で「問題がない」と判断できるのかを整理する。
type Worker struct {
  jobs    <-chan int
  results chan<- int
  ctx     context.Context
}
func (w *Worker) Start() {
  go func() {
    for {
      select {
      case job, ok := <-w.jobs:
        if !ok {
          return
        }
        result := w.process(job)
        w.results <- result
      case <-w.ctx.Done():
        return
      }
    }
  }()
}
func (w *Worker) process(job int) int {
  time.Sleep(100 * time.Millisecond)
  return job * 2
}技術的ポイント整理
- 終了保証の明示性
w.ctx.Done()とw.jobsのclose検知により確実にループ終了できる設計。
 - チャネル駆動型の責務分離
- jobの投入・処理・結果返却がチャネル単位で明確に分離。
 
 - ループ内クロージャ変数の正当性
- ループ変数を直接使わず、クロージャキャプチャの競合を生んでいない。
 
 - キャンセル設計の伝播
context.Contextを外部から渡して統制可能。
 - エラーを握りつぶさない構造
process()が失敗を返さない構造。必要なら返却型変更で通知設計可能。
 
この例のように"ライフサイクルが完全に統制されたgoroutine"が設計上の理想である。
2. 問題のある実装とレビュー指摘集
2-1. fire-and-forgetの安易な使用
go log.Println("処理開始")
@Reviewer非同期化の目的が不明確です。fire-and-forget型ですが、ログの順序保証や出力漏れが発生する可能性があります。同期的に処理するか、明示的に非同期目的を整理してください。
2-2. WaitGroupによる「意味のない同期化」
var wg sync.WaitGroup
for _, id := range ids {
  wg.Add(1)
  go func(id int) {
    defer wg.Done()
    process(id)
  }(id)
}
wg.Wait()
@Reviewer全ての処理が完了するまで強制的に待機する構造になっています。同期化による性能向上の効果が薄くなる場合があるため、逐次実行と比較検討してください。
2-3. context未伝播
func Handle(ctx context.Context) {
  go worker()
}
@Reviewer`ctx` が子goroutineに渡されていません。キャンセル設計が機能不全になります。workerに`ctx`を渡してキャンセル制御を統一してください。
2-4. クロージャ変数競合
for _, user := range users {
  go func() {
    fmt.Println(user.Name)
  }()
}
@Reviewerクロージャ内でループ変数`user`を参照しています。goroutine起動タイミングで競合が生じる可能性があります。ループ内で`user := user`とスコープ固定してください。
2-5. goroutineリークパターン
go func() {
  <-ctx.Done()
}()
@Reviewer`ctx.Done()`待機だけでは、親での`WithCancel()`実装が漏れるとリークします。必ず親のキャンセル設計が存在するか確認してください。
2-6. エラー握りつぶし
go func() {
  err := external()
  if err != nil {
    log.Println("fail")
  }
}()
@Reviewerローカルでエラーログ出力のみ行われています。上位へのエラー通知手段がないため、channel通知やcallback設計で回収ルートを整備してください。
3. チェックリスト:レビュー観点整理
| 観点 | 確認内容 | 
|---|---|
| 終了保証 | context伝播/チャネルclose判定 | 
| エラー処理 | 上位層通知設計有無 | 
| クロージャ競合 | ループ変数捕捉の競合有無 | 
| 同期制御 | WaitGroup・select設計妥当性 | 
| リーク対策 | 永続待機ブロック排除 | 
| 責務分離 | 異層混在排除 | 
| 実質目的 | 本当に非同期が必要か設計意図確認 | 
4. ケーススタディ:文法上は正しいが設計破綻する例
func Watch(ctx context.Context, ch <-chan string) {
  go func() {
    for {
      select {
      case msg := <-ch:
        log.Println("received:", msg)
      case <-ctx.Done():
        return
      }
    }
  }()
}
@Reviewer`ch`がcloseされた場合の処理が考慮されていません。またこの関数が複数回呼ばれるとgoroutineが積み上がる構造です。close検知追加およびgoroutine統制設計を検討してください。
5. context終了伝播モデル
6. 実運用で多発するgoroutine系障害
事例A: HTTPクライアント呼び出しの非同期乱用
go func() {
  resp, err := http.Get(url)
  if err == nil {
    cache.Set(key, resp)
  }
}()
@ReviewerI/O処理はgoroutine大量生成するとソケット逼迫・FD上限超過が発生します。Pool化・同時実行数制限・timeout設計を併用してください。
7. ワーカーパターンの安定設計
type Job struct{ ID int }
func worker(jobs <-chan Job) {
  for job := range jobs {
    process(job)
  }
}
func main() {
  jobs := make(chan Job)
  for i := 0; i < 5; i++ {
    go worker(jobs)
  }
}- goroutine数が制御可能
 - Channel閉じが終了判定になる
 - FD/CPU等のリソース管理が安定する
 
8. 代替設計検討の視点
| goroutine | 代替可能設計例 | 
|---|---|
| 軽量通知系 | time.AfterFunc | 
| スケジューラ | cronライブラリ等 | 
| ワーカープール | channel駆動設計 | 
| 非同期I/O | context+http.Client | 
9. レビューアー育成用思考テンプレート
- 本当に非同期が要るのか?
 - goroutineの起動数を制御可能か?
 - キャンセル手段が存在するか?
 - 外部資源(I/O/FD/メモリ)の開放保証は?
 - エラーは握り潰されていないか?
 - 他層責務との境界は明確か?
 
10. 最後に:レビューとは「設計の物語」を読む訓練
goroutineレビューは構文ではなく設計を読む行為である。
非同期化の背後にある意図・制約・妥協点を読解し、事故の芽を摘む。
これこそが現場のレビューアーに必要な「構造的読解力」である。

