Go言語のGoroutineリーク防止策とContextキャンセルのパターン
Go Backend Concurrency Performance
結論
バッファ付きチャネル(make(chan T, 1))か select で <-ctx.Done() を監視してリークを防ぎます。
// Go公式仕様:context.Context による安全な Goroutine キャンセルパターン
package main
import (
"context"
"time"
)
function fetchData(ctx context.Context) (string, error) {
// ⭕ バッファ数 1 を確保し、呼び出し側が先に早期 return しても送信ブロックを起こさない
ch := make(chan string, 1)
go func() {
// 重い処理...
res := "data"
select {
case ch <- res:
case <-ctx.Done(): // ⭕ 親がキャンセルされた場合、即座に Goroutine を終了
return
}
}()
select {
case res := <-ch:
return res, nil
case <-ctx.Done():
return "", ctx.Err() // タイムアウト等のエラー
}
}
Go公式仕様:なぜ Goroutine リークが発生するのか?
Go公式ドキュメント(go.dev/doc/diagnostics)の規定通り、Goのガベージコレクタ(GC)は、チャネル(Channel)の送信・受信待ちで無限ブロック(Blocked)された Goroutine のスタック領域および内部変数を回収(Reclaim)できません。
呼び出し側がエラー等で途中で return してしまい、取り残された Goroutine が終了条件を満たせない場合、その Goroutine は永久にメモリ内に居座り続けます。
実際に起こる障害:非バッファチャネルと早期 return によるメモリリーク
// ❌ 事故:Goroutine リークを起こすダメな実装
func badFetch() string {
ch := make(chan string) // バッファなしチャネル
go func() {
ch <- "result" // ❌ 受信者がいなくなると永久にブロック! (Goroutine Leak)
}()
if someCondition {
return "default" // 早期 return すると、上の Goroutine が死ぬまでブロックされる
}
return <-ch
}
このリクエストが1秒間に数百回呼ばれるWebサービスで発生すると、数万個の Goroutine がメモリを食い潰し、Goプロセスが OOM (Out of Memory) で突然クラッシュする大障害 が発生します。
予防手順
- 1回限りの結果受信(Goroutine から1つだけ値を受け取る)の場合、必ず
make(chan T, 1)のように容量1以上のバッファ付きチャネルを使用する - 長時間動作する非同期処理には
context.WithTimeoutまたはcontext.WithCancelを伝播させる - 子 Goroutine 内のチャネル送信部で
selectを使い、case <-ctx.Done():のキャンセル通知を必ず監視して脱出させる