bigywater.tech
Go · 63

Ticker 与 time.After 泄漏:Go 1.23 之后改了什么

“循环里用 time.After 会泄漏”是老 Go 的经典坑。Go 1.23 起文档说垃圾回收器能回收没有引用的定时器。这篇在 1.27.1 上实测它是否还泄漏,发现旧行为本机已经复现不出来,并量出仍然存在的分配成本。

0.0 MB
丢掉 10 万个不 Stop 的 NewTicker,GC 之后堆的增长
25.8 MB
同样 10 万个 Ticker 放进切片保留之后的增长
145 vs 83 ns
热循环每轮 time.After 与复用 Timer 的耗时

没有引用就能回收

实测 · 本机执行输出
  100000 个 NewTicker(1h) 丢掉不 Stop:GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  100000 个 time.Tick(1h) 丢掉:GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  100000 个 NewTimer(1h) 丢掉不 Stop:GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  循环 100000 次,每次 select 里放一个 time.After(1h):GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  100000 个 NewTicker(1h) 放进切片保留(对照):GC 之后堆 0.2 MB → 26.0 MB(增加 25.8 MB)

在 go.mod 写 go 1.27 的程序里,丢掉不 Stop 的 10 万个 NewTicker、time.Tick、NewTimer,以及循环里 10 万次 select 放 time.After,GC 之后堆都没有增长;把 Ticker 保留在切片里就增长 25.8 MB。可见回收是靠“没有引用”,不是靠 Stop。

旧泄漏我复现不了

实测 · 本机执行输出
  [go 1.27 + GODEBUG=asynctimerchan=1] fatal error: removed GODEBUG "asynctimerchan" set to old value "1" in environment (https://go.dev/doc/godebug#go-127)
  [go.mod 写 go 1.22] 100000 个 NewTicker(1h) 丢掉不 Stop:GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  [go.mod 写 go 1.22,select+time.After 循环] 循环 100000 次,每次 select 里放一个 time.After(1h):GC 之后堆 0.2 MB → 0.2 MB(增加 0.0 MB)
  godebug.md:171: Go 1.27 removed the `asynctimerchan` setting, as noted in the [Go 1.23](#go-123) section.
  godebug.md:358: This setting will be removed in Go 1.27.

老版本的泄漏需要 asynctimerchan=1 之类的旧行为。Go 1.27 把这个设置删了(设置它会直接 fatal error),go.mod 写 go 1.22 的同一程序也不泄漏。我机器上只有 1.27.1 工具链,所以“Go 1.23 之前确实泄漏”这一点只来自官方文档的描述,我没有实测。

成本还在

实测 · 本机执行输出
  每轮 select 里放 time.After(1h):3 次分配,145 ns/轮;复用一个 Timer 并 Reset:0 次分配,83 ns/轮(各 1000000 轮)

不泄漏不代表便宜:热循环里每轮 time.After 要 3 次分配、145 ns,复用一个 Timer 并 Reset 是 0 次分配、83 ns。每轮处理量很小的循环里,这个差别才值得处理;一般代码用 time.After 就够了,文档也这么说。

Ticker 仍会丢 tick

实测 · 本机执行输出
  周期 10 ms 的 Ticker,接收方每次处理 55 ms:600 ms 里收到 11 个 tick(按周期应有 60 个)

接收方比周期慢时,Ticker 不会攒着补发:周期 10 ms、每次处理 55 ms,600 ms 只收到 11 个 tick。需要“一个都不能少”的场景不能靠 Ticker。

什么时候还要 Stop

实测 · 本机执行输出
  go doc time.NewTicker(节选):
    NewTicker returns a new Ticker containing a channel that will send the
    current time on the channel after each tick. The period of the ticks is
    specified by the duration argument. The ticker will adjust the time interval
    or drop ticks to make up for slow receivers. The duration d must be greater
    than zero; if not, NewTicker will panic.
    Before Go 1.23, the garbage collector did not recover tickers that had not
    yet expired or been stopped, so code often immediately deferred t.Stop
    after calling NewTicker, to make the ticker recoverable when it was no
    longer needed. As of Go 1.23, the garbage collector can recover unreferenced
    tickers, even if they haven't been stopped. The Stop method is no longer
    necessary to help the garbage collector. (Code may of course still want to
    call Stop to stop the ticker for other reasons.)
  go doc time.After(节选):
    After waits for the duration to elapse and then sends the current time on
    the returned channel. It is equivalent to NewTimer(d).C.
    Before Go 1.23, this documentation warned that the underlying Timer
    would not be recovered by the garbage collector until the timer fired,
    and that if efficiency was a concern, code should use NewTimer instead and
    call Timer.Stop if the timer is no longer needed. As of Go 1.23, the garbage
    collector can recover unreferenced, unstopped timers. There is no reason to
    prefer NewTimer when After will do.

文档的说法是 Stop 不再是为了让 GC 回收,但你仍可能因为别的原因想停掉它,比如不再需要它产生事件。这一点我没有再单独设计实验。

分步演示

Ticker 与 time.After 泄漏
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。堆数字 = 两次 runtime.GC() 后的 HeapAlloc,每种 10 万个;go.mod 写 go 1.22 的对照由同一个工具链(1.27.1)编译;对照实验“保留在切片里”用来确认测量方法能看到增长;热循环成本各 100 万轮,单次测量;读过的文档:go doc time.NewTicker / time.After,godebug.md 中 asynctimerchan 的两处
    • 没有验证:Go 1.23 之前的泄漏行为(本机没有旧工具链)、1.23 之后 GC 回收定时器的具体机制、time.Tick 在长期运行服务里的实际影响、在多 goroutine 并发创建下的表现、Stop 的其它用途(此处没有单独实验)
    京ICP备2026067502号-1京公网安备11010502063487号