在 Go 语言 1.23 版本之前,与Timer(定时器)关联的通道是异步的(有缓冲,容量为 1)。这意味着即使在调用Timer.Stop(停止定时器)或Timer.Reset(重置定时器)并返回后,仍可能接收到过期的时间值。
换句话说,在 Go 1.23 之前,由于定时器的通道特性,可能会出现这样一种情况:即使已经尝试停止或重置定时器,但仍有可能从该定时器的通道中接收到已经过时的时间触发信号,因为这个通道的异步和缓冲特性可能导致一些操作响应不及时或出现意外的信号接收。而在 Go 1.23 版本及之后,可能对定时器的这种行为进行了调整或改进,以避免出现接收过期时间值的情况。
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
//
// Before Go 1.23, the garbage collector did not recover
// timers that had not yet expired or been stopped, so code often
// immediately deferred t.Stop after calling NewTimer, to make
// the timer recoverable when it was no longer needed.
// As of Go 1.23, the garbage collector can recover unreferenced
// timers, even if they haven't expired or 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 timer for other reasons.)
//
// Before Go 1.23, the channel associated with a Timer was
// asynchronous (buffered, capacity 1), which meant that
// stale time values could be received even after [Timer.Stop]
// or [Timer.Reset] returned.
// As of Go 1.23, the channel is synchronous (unbuffered, capacity 0),
// eliminating the possibility of those stale values.
//
// The GODEBUG setting asynctimerchan=1 restores both pre-Go 1.23
// behaviors: when set, unexpired timers won't be garbage collected, and
// channels will have buffered capacity. This setting may be removed
// in Go 1.27 or later.
go 1.23之前 timer.Reset和go 1.23之后 timer.Reset 区别
在 Go 1.23 之前,与计时器相关联的通道是异步的(缓冲,容量为 1),这意味着即使在 Timer.Stop 或 Timer.Reset 返回后,也能接收到过期的时间值。对于使用 NewTimer 创建的计时器,只有在通道(channel)耗尽的定时器停止或过期时才会调用重置。例如,在 Go1.22 及以前的版本上,结合程序来看,计时器 t 的超时时间为 50 毫秒。在 Sleep 方法等待 100 毫秒后,计时器 t 早已经过期,向 t.C 通道发送了一个值。但由于 Reset 方法并不会耗尽通道,因此 <-t.C 不会阻塞,并立即继续程序。
而在 Go 1.23 中,该通道是同步通道(无缓冲,容量为 0),从而消除了出现过期时间值的可能性。Go 1.23 对 time.Timer 和 time.Ticker 的实现进行了重大更改。在 Go 1.23 中,任何调用 Reset 或 Stop 方法的操作,在该调用之前准备的任何过时值都不会在调用后被发送或接收。新的行为仅在主 Go 程序位于使用 Go 1.23.0 或更高版本的 go.mod 文件的模块中时才会启用。当 Go 1.23 构建旧程序时,旧行为仍然有效。新的 GODEBUG 设置 asynctimerchan=1 可以在即使程序在其 go.mod 文件中指定了 Go 1.23.0 或更高版本时,也恢复到异步通道行为。此外,Go 1.23 版本正式发布后,Russ Cox 解决了一直困扰 Go 团队的 Timer/Ticker 的 GC 回收问题,进而解决了 Timer 的 Stop 和 Reset 很难正确使用的问题。
Go1.23 之前 timer.reset 行为
在 Go1.23 之前,对于使用 NewTimer 创建的计时器,只有在通道(channel)耗尽的定时器停止或过期时才会调用重置。例如,在一些特定的场景下,若计时器未处于特定状态就调用 Reset 方法,可能会导致不可预期的结果。这种行为在实际应用中可能会给开发者带来一些困扰,因为开发者需要时刻关注计时器的状态,以确保在正确的时机调用 Reset 方法。这不仅增加了开发的复杂性,还可能导致一些难以排查的错误。比如在一些需要频繁重置计时器的场景中,开发者可能需要额外编写复杂的逻辑来判断计时器的状态,以避免出现错误的重置操作。
Go1.23 之后 timer.reset 行为
在 Go1.23 中,Reset 方法的行为得到了改进。Reset 使计时器重新开始计时,(本方法返回后再)等待时间段 d 过去后到期。如果调用时计时器还在等待中会返回真;如果计时器已经到期或者被停止了会返回假。这一改进使得计时器的使用更加直观和方便。开发者不再需要像在 Go1.23 之前那样时刻关注计时器的状态来判断是否可以进行重置操作。现在,只需要简单地调用 Reset 方法,就可以让计时器重新开始计时。这大大简化了开发过程,减少了出错的可能性。同时,这一改进也提高了代码的可读性和可维护性,使得开发者能够更加专注于业务逻辑的实现,而不是花费大量时间在处理计时器的复杂状态上。
Go1.23 对 timer.reset 的改进使得 Go 语言在计时器的使用上更加便捷和可靠。这一改进体现了 Go 语言不断发展和完善的趋势,为开发者提供了更好的开发体验。
示例
func TestDemo(d *testing.T) {t := time.NewTimer(time.Millisecond * 40)// t已经触发,t.C已经有值time.Sleep(time.Millisecond * 100)start := time.Now()// 对于使用 NewTimer 创建的计时器,只有在 channel 耗尽的定时器停止或过期时才会调用重置// 这里定时器没有被耗尽// Reset 方法并不会耗尽通道,因此 <-t.C 不会阻塞,并立即继续程序ok := t.Reset(time.Millisecond * 40)// 1.23之前是false,false表示已过期/停止// 1.23 中,任何调用 Reset 或 Stop 方法的操作,在该调用之前准备的任何过时值都不会在调用后被发送或接收fmt.Println("ok:", ok)// 等待过期// 1.23之前,立马过期,等待0ms,应为过期的未读取的信号未被清空// 1.23及之后,Reset 会清空过期的未读取的信号,然后等待40ms过期<-t.Cfmt.Printf("wait:%dms\n", time.Since(start).Milliseconds())
}// 在Go1.23中,创建timer之后,Reset会重置timer,可以放心的当成一个新的timer来使用。
// Reset只要没被触发都会返回true,