Go: 「ループ内の time.After はタイマーがリークする」は Go 1.23 で解消済み
Go 1.22 までは、select の中で毎回 time.After を呼ぶと、発火するまでタイマーが GC されずメモリが積み上がった。Go 1.23 からは、参照されなくなったタイマーは発火前でもすぐ回収される。Stop() 後のチャネルの読み捨ても不要になった。
背景
X では今も「time.After をループで使うな、time.NewTimer にして Stop() + チャネルを drain しろ」という tip が流れてくる(出典の投稿もそう)。これは Go 1.22 以前の話。
go
for {
select {
case msg := <-ch:
handle(msg)
case <-time.After(30 * time.Second): // 1.22 以前: 30秒経つまで毎回のタイマーが残る
return errIdle
}
}Go 1.23 での変更
Stopを呼んでいなくても、どこからも参照されていないTimer/Tickerはすぐ GC の対象になる。- タイマーのチャネルがバッファなし(同期)になり、
Reset/Stopの後に古い値を受け取ることがなくなった。そのため「Stop()が false なら<-t.Cで読み捨てる」というイディオムは不要。 - 新しい挙動になるのは
go.modのgo行が 1.23 以上のモジュールだけ。古いgo行のままなら以前の挙動が続く(GODEBUG=asynctimerchan=1で旧挙動に戻すことも可能)。
それでも NewTimer を使う場面
NewTimer を- ループが非常に速く回る場合は、毎回タイマーを作るより1つを
Resetして使い回すほうが割り当てが少ない。 - 「最後のメッセージから30秒」のようにタイマーを延長したい場合は、もともと
Resetが自然。
確認
bash
grep '^go ' go.mod # 1.23 以上か参考
- Go 1.23 リリースノート(Timer の変更): go.dev
出典
- @amitsinha__07 2026-10-15(Go 1.22 以前向けの tip。上記の補足付きで記録): @amitsinha__07 · 2026-10-15