テック技報最新号へ →
コード

Go: 「ループ内の time.After はタイマーがリークする」は Go 1.23 で解消済み

Go 1.22 までは、select の中で毎回 time.After を呼ぶと、発火するまでタイマーが GC されずメモリが積み上がった。Go 1.23 からは、参照されなくなったタイマーは発火前でもすぐ回収される。Stop() 後のチャネルの読み捨ても不要になった。

@amitsinha__07 · 2026-10-16
#go#time#timer#go1.23

背景

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 を使う場面

  • ループが非常に速く回る場合は、毎回タイマーを作るより1つを Reset して使い回すほうが割り当てが少ない。
  • 「最後のメッセージから30秒」のようにタイマーを延長したい場合は、もともと Reset が自然。

確認

bash
grep '^go ' go.mod   # 1.23 以上か

参考

  • Go 1.23 リリースノート(Timer の変更): go.dev

出典

X の元の投稿を開く