作者:陈明勇
个人网站:https://chenmingyong.cn
文章持续更新,如果本文能让您有所收获,欢迎点赞收藏加关注本号。 微信阅读可搜《程序员陈明勇》。 这篇文章已被收录于 Github,欢迎大 家Star 催更并持续关注。
前言
Go 1.23 版本在北京时间 2024 年 8 月 14 日凌晨 1:03 发布。该版本带来了多项重大更新,具体内容可以参考我之前的文章:Go 1.23 版本发布啦,这些重大更新你一定要知道!。本文将重点介绍其中关于定时器(Timer 和 Ticker)的优化。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

Timer 和 Ticker 的基本概念
在深入探讨 Go 1.23 版本对 Timer 和 Ticker 定时器进行的优化之前,有的读者可能需要了解这两种定时器的基础知识。以下是关于这两种定时器的基本介绍:
-  Timer是一个一次性的定时器,用于在未来的某一时刻执行一次操作。常用于单次延迟执行任务。
-  Tciker是一个周期性的定时器,用于在固定的时间间隔重复执行任务。它在每个间隔时间到来时,向其通道(Channel)发送当前时间。常用于重复执行任务。
垃圾回收的改进
- Go 1.23 之前的行为: 如果一个 Timer或Ticker没有被显式调用Stop方法,即使程序不再引用它们,它们也不会立即被垃圾回收。Timer会在触发后被回收,而Ticker则从来不会被自动回收。
- Go 1.23 新行为: 如果程序不再引用一个 Timer或Ticker(即没有其他部分的代码持有它们的引用),即使没有调用Stop方法,它们也会有资格立即被垃圾回收。这可以减少内存泄漏的风险,因为不再需要显式调用Stop也可以保证资源会被回收。
这一更新提高了内存管理效率。以前,如果你创建了一个 Timer 或 Ticker,但忘记调用 Stop,这些对象会一直占用内存,直到程序结束。而现在,只要程序不再引用这些对象,它们就会被回收,这样可以避免内存泄漏的问题。
计时器通道行为的变化
- Go 1.23 之前的行为: 与 Timer或Ticker关联的通道带有一个元素缓冲区,这导致Reset或Stop方法在调用后,可能仍会接收到之前准备好的旧值,造成使用上的困难。
- Go 1.23 新行为: 计时器通道变成了无缓冲的(容量为 0)。这意味着在调用 Reset或Stop方法后,Go 可以保证不会再接收到旧的值。这使得Reset和Stop的使用更加可靠。
- 副作用: 由于通道现在是无缓冲的,len和cap操作返回的值变成了 0,而不是 1。这可能会影响那些依赖轮询通道长度来判断是否能成功接收值的代码。为了适应这种变化,代码应该使用 非阻塞 的接收操作来替代。
这一更新让定时器操作更加可靠和安全。 在 Go 1.23 之前,Timer 和 Ticker 的通道是有缓冲的,这意味着即使你调用了 Reset 或 Stop,通道中仍可能残留旧的定时信号,这会导致潜在的竞态条件问题。现在改为无缓冲通道后,Go 保证了调用 Reset 或 Stop 后,通道不会再收到旧的数据。
我们来看看下面的代码在不同 Go 版本里的运行情况:
package main
import (
	"fmt"
	"time"
)
func main() {
	// 程序退出信号
	quit := make(chan bool)
	timer := time.NewTimer(2 * time.Second)
	go func() {
		// 确保定时器已触发并发送信号
		time.Sleep(4 * time.Second)
		// 试图读取通道,看是否有值
		select {
		case t := <-timer.C:
			fmt.Println("接收到定时器信号:", t.Format(time.DateOnly))
		default:
			fmt.Println("无信号")
		}
		quit <- true
	}()
	// 确保定时器已触发并发送信号
	time.Sleep(3 * time.Second)
	wasStopped := timer.Stop()
	if wasStopped {
		// Go 1.23 或更高版本会走这条分支
		fmt.Println("定时器未过期,停止成功")
	} else {
		// Go 1.23 以前的版本会走这条分支
		fmt.Println("定时器已经过期并且信号已经发送")
	}
	// 等待退出信号
	<-quit
}
在 Go 1.22 及之前的版本的运行结果:
定时器已经过期并且信号已经发送
接收到定时器信号: 2024-08-20
由于通道是有缓冲的,在定时器过期时已经发送了信号,因此即使在定时器触发之后调用 Stop() 方法,我们仍然可以从缓冲中接收到信号。
在 Go 1.23 或更高版本的运行结果:
定时器未过期,停止成功
无信号
由于通道是无缓冲的,信号发送是一个阻塞操作。如果在信号被接收之前调用 Stop() 方法,这将阻止信号的发送。因此,定时器被成功停止,Stop() 返回 true。
注意事项
对于 Timer 和 Ticker 的这些新行为只有在 Go 模块使用 go.mod 文件并且指定了 Go 1.23.0 或更高版本时才会生效。也就是说如果你的 Go 版本是 Go 1.23,但是你在 go.mod 文件里指定的 Go 版本小于 Go 1.23,那么这些新行为不会生效。
此外,如果你在 go.mod 文件里指定的 Go 版本大于等于 Go 1.23,你可以通过设置环境变量 GODEBUG 的 asynctimerchan=1,从而恢复到之前异步通道的行为。
小结
本文详细介绍了在 Go 1.23 版本中对 Timer 和 Ticker 的重要优化,包括两个主要方面:垃圾回收的改进 和 计时器通道行为的变化。改进后的垃圾回收机制有助于防止内存泄漏,而计时器通道的调整则确保在调用 Reset 或 Stop 之后,通道不会接收到任何旧数据,提高了定时器操作的可靠性和安全性。
后续将会分主题发布更多关于 Go 1.23 的详细更新内容。关注我,不错过任何精彩内容。



















