未停止但不再被引用的定时器和周期计时器现在可以被垃圾回收。在 Go 1.23 之前,未停止的定时器在触发之前无法被垃圾回收,而未停止的周期计时器则永远无法被垃圾回收。Go 1.23 的实现避免了那些没有使用 t.Stop 的程序中的资源泄漏。
定时器通道现在是同步的(无缓冲),这为 t.Reset 和 t.Stop 方法提供了更强的保证:在这些方法返回后,定时器通道的接收操作不会观察到与旧定时器配置相对应的过期时间值。在 Go 1.23 之前,使用 t.Reset 时无法避免过期值,而使用 t.Stop 避免过期值则需要谨慎处理其返回值。Go 1.23 的实现完全消除了这个问题。
if len(t.C) == 1 { <-t.C // 更多代码 }应该改为:
select { default: case <-t.C: // 更多代码 }Select 竞态
c := make(chan bool) close(c) select { case <-c: println("done") case <-time.After(1*time.Nanosecond): println("timeout") }当 select 参数被求值并且 select 检查相关通道时,定时器应该已经过期了,这意味着两个 case 都已准备就绪。Select 通过随机选择来处理多个就绪的 case,所以这个程序理论上应该大约各执行一半的情况。由于 Go 1.23 之前的定时器实现中的调度延迟,像这样的程序错误地 100% 执行了 "done" case。
select { case <-ctx.Done(): return nil case <-time.After(timeout): return errors.New("timeout") }然后测试会将 timeout 设置为 1ns,如果代码返回错误就会失败。要修复这样的测试,要么修改调用者使其理解可能发生超时,要么修改代码使其在超时情况下也优先考虑 done 通道,像这样:
select { case <-ctx.Done(): return nil case <-time.After(timeout): // 在测试过程中出现短超时时 // 再次检查 Done 是否已就绪 select { default: case <-ctx.Done(): return nil } return errors.New("timeout") }调试
GODEBUG=asynctimerchan=0 mytest # 强制使用 Go 1.23 定时器 GODEBUG=asynctimerchan=1 mytest # 强制使用 Go 1.22 定时器如果程序或测试在使用 Go 1.22 时始终通过,但在使用 Go 1.23 时始终失败,这很可能表明问题与定时器有关。在我们观察到的所有测试失败中,问题都出在测试本身,而不是定时器实现。因此,下一步是要准确识别 mytest 中哪些代码依赖于旧实现。为此,你可以使用 bisect 工具:
# 堆代码 duidaima.com go install golang.org/x/tools/cmd/bisect@latest bisect -godebug asynctimerchan=1 mytest以这种方式调用时,bisect 会反复运行 mytest,根据导致定时器调用的堆栈跟踪来切换新旧定时器实现。通过二分查找,它可以将失败缩小到在特定堆栈跟踪期间启用新定时器,并报告这些跟踪。在 bisect 运行时,它会打印试验的状态信息,主要是为了在测试很慢时让你知道它仍在运行。