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レビューは構文ではなく設計を読む行為である。
非同期化の背後にある意図・制約・妥協点を読解し、事故の芽を摘む。
これこそが現場のレビューアーに必要な「構造的読解力」である。