目次
この記事が解決する悩み
- goroutineリークを疑っているけど、どのgoroutineがどこで止まっているのか特定できない
- 通常のgoroutineプロファイルは全goroutineを出すだけで、リークの判別は人力になる
- 本番サーバに手軽なリーク検知を仕込みたい
結論
Go 1.27でgoroutineleakプロファイルが正式公開になりました。GCが「二度と再開できないgoroutine」を自動で見つけてくれます。time.Sleep(500 * time.Millisecond) // goroutineがブロックに入ってからGCを回す
runtime.GC()
p := pprof.Lookup("goroutineleak")
p.WriteTo(os.Stdout, 1)
// 実行結果(リーク4件のプログラムでの例)
// goroutineleak profile: total 4
リーク検出の仕組み — GCの到達可能性
公式リリースノートでのリークの定義は「channel、sync.Mutex、sync.Condなどでブロックされたまま、そのプリミティブが二度と利用可能にならないgoroutine」です。 検出はGCの仕事です。goroutine G がプリミティブ P でブロックしているとき、P が「実行中のgoroutine、およびそれらが再開できるgoroutine」のどこからも到達できないなら、P は二度と空かないので G は永遠に止まったまま、と判定できます。 裏を返すと、判定は到達可能性ベースなので P がグローバル変数から届いてしまうと検出を逃します。この限界は後ほど実測で確かめますね。実測1: 早期returnでgoroutineを置き去りにする
リリースノートが挙げている典型例を再現します。ワーカーgoroutineがエラーを送ってきたら早期returnして、残りの結果を受け取らないパターンです。func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan workResult)
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- workResult{id: res.id, err: err}
}(w)
}
var results []workResult
for range len(ws) {
r := <-ch
if r.err != nil {
// 早期returnで残りのgoroutineを置き去りにする
return nil, r.err
}
results = append(results, r)
}
return results, nil
}
func main() {
_, err := processWorkItems([]workItem{{1}, {2}, {3}, {4}, {5}})
fmt.Println("returned:", err)
time.Sleep(500 * time.Millisecond) // 全goroutineが送信ブロックに入ってから
runtime.GC()
p := pprof.Lookup("goroutineleak")
fmt.Print("goroutineleak: ")
p.WriteTo(os.Stdout, 1)
// 実行結果
// returned: item 1 failed
// goroutineleak: goroutineleak profile: total 4
// 4 @ 0x7ff61afcc52a 0x7ff61af60f5c 0x7ff61af60b57 0x7ff61b038229 0x7ff61afd2601
// # 0x7ff61b038228 main.processWorkItems.func1+0x48 C:/Workspace/eeat-verify/goroutineleak/leak/main.go:34
}
ch <- の送信行(main.go:34)を指していました。
リークしたgoroutineの特定まで自動でやってくれるのは助かりますね。
実測2: 全結果を受け切るように直す
直し方はシンプルで、channelをバッファ付きにして全結果を受け切ってから返すだけです。 ch := make(chan workResult, len(ws)) // バッファ付きにする
var failed error
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- workResult{id: res.id, err: err}
}(w)
}
var results []workResult
for range len(ws) {
r := <-ch
if r.err != nil && failed == nil {
failed = r.err // 記録して受け続ける
}
results = append(results, r)
}
if failed != nil {
return nil, failed
}
return results, nil
// 実行結果
// returned: item 1 failed
// goroutineleak: goroutineleak profile: total 0
実測3: グローバル変数に置くと検出を逃す
到達可能性ベースの限界です。channelをパッケージ変数にすると、GCから見て常に「届く」場所にあるのでリーク判定が出ません。var ch chan int // パッケージレベルのグローバル変数
func main() {
ch = make(chan int)
go func() {
ch <- 42 // 誰も受け取らない
}()
time.Sleep(500 * time.Millisecond)
runtime.GC()
p := pprof.Lookup("goroutineleak")
fmt.Print("goroutineleak: ")
p.WriteTo(os.Stdout, 1)
// 実行結果
// goroutineleak: goroutineleak profile: total 0
}
net/http/pprof のエンドポイントで取る
net/http/pprofをimportしておくだけで、/debug/pprof/goroutineleakエンドポイントが使えるようになります。curl -s "http://localhost:8791/debug/pprof/goroutineleak?debug=1"
# 実行結果(抜粋・HTTP 200)
# goroutineleak profile: total 2
# 1 @
# # 0x0
#
# 1 @ 0x7ff6f6fa798a 0x7ff6f6f34b3c 0x7ff6f6f34737 0x7ff6f71a1fbe 0x7ff6f6fae8c1
# # 0x7ff6f71a1fbd main.main.func1+0x1d C:/Workspace/eeat-verify/goroutineleak/httpleak/main.go:14
ch <- 42 で送信待ち)がスタック付きで出てきました。1 @ 0x0 のようにシンボルが解決できないエントリが混ざることもあります。
本番サーバでもこのエンドポイントを定期的に見れば、リークの早期発見につながりますよ。
つまずきポイント
- 検出はGCのタイミングで更新されます。短いバッチやテストでは runtime.GC() を明示的に呼んでから読むのが確実です(実測でもGCを挟むまで total 0 のままでした)。
- goroutineがtime.Sleep中の間は検出されませんでした。検出条件は「channelやmutexなどでブロック中」であることです。
- WriteToのdebug引数に0を渡すとgzip圧縮のprotobufが出ます。テキストで読むなら1以上を渡してください。
- GOEXPERIMENT=goroutineleakprofileは1.27で削除済みです。設定していても効果はありません。
まとめ
| 項目 | goroutine プロファイル | goroutineleak プロファイル |
|---|---|---|
| 出力対象 | 現在の全goroutine | 検出されたリークgoroutineのみ |
| リークの判別 | 人力(スタックを読む) | ランタイムが自動判定 |
| 限界 | 件数が多くて読みにくい | グローバル変数から届くプリミティブは検出を逃す |
| 取得方法 | pprof.Lookup(“goroutine”) | pprof.Lookup(“goroutineleak”) または /debug/pprof/goroutineleak |
【広告】お名前.comならドメイン取得が格安。![]()
検証環境
| OS | Windows 11 (build 10.0.26200) |
|---|---|
| CPU | AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics |
| メモリ | 28GB |
| 言語/ツール | go1.27.1 windows/amd64 |
| 使用コマンド | go run ./leak / ./noleak / ./globalleak / httpleak に対する curl |
| 測定日 | 2026-09-25 |

あわせて読みたい
参考
- Go 1.27 is released — The Go Blog(公式)
- Go 1.27 Release Notes — Goroutine leak profile(公式)
- Go 1.26 Release Notes — Experimental goroutine leak profile(公式・リーク実例)
- runtime/pprof — Go パッケージドキュメント(公式)
【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!![]()

コメント