Go1.23 新特性:花了近 10 年,time.After 终于不泄漏了!

大家好,我是煎鱼。

好多年前,我写过 timer.After 的使用和坑。Go 这么多年以来这块一直有内存泄露。有的同学或多或少都有遇到过。

最近 Go1.23 即将正式发布,Go 核心团队负责人 rsc 自述花了将近 10 年的努力,终于把这个问题修复了。值得我们关注!

timer.After 是什么

这是之前编写的部分,我测试验证了下。在 Go1.22 依然有效,仍然是有问题的。因此没有做什么修改。主要是给大家做知识温习回顾的作用。

今天是男主角是 Go 标准库 time 所提供的 After 方法。函数签名如下:

func After(d Duration) <-chan Time

该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。

在常见的场景下,我们会基于此方法做一些计时器相关的功能开发,例子如下:

func main() {
    ch := make(chan string)
    go func() {
        time.Sleep(time.Second * 3)
        ch <- "脑子进煎鱼了"
    }()

    select {
    case _ = <-ch:
    case <-time.After(time.Second * 1):
        fmt.Println("煎鱼出去了,超时了!!!")
    }
}

在运行 1 秒钟后,输出结果:

煎鱼出去了,超时了!!!

上述程序在在运行 1 秒钟后将触发 time.After 方法的定时消息返回,输出了超时的结果。

有什么问题和坑

从例子来看似乎非常正常,也没什么 “坑” 的样子。莫非是虚晃一枪?

我们再看一个不像是有问题例子,这在 Go 工程中经常能看见,只是大家都没怎么关注。

代码如下:

func main() {
    ch := make(chan int, 10)
    go func() {
        in := 1
        for {
            in++
            ch <- in
        }
    }()

    for {
        select {
        case _ = <-ch:
            // 煎鱼干了点什么...
            continue
        case <-time.After(3 * time.Minute):
            fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())
        }
    }
}

在上述代码中,我们构造了一个 for+select+channel 的一个经典的处理模式。

同时在 select+case 中调用了 time.After 方法做超时控制,避免在 channel 等待时阻塞过久,引发其他问题。

看上去都没什么问题,但是细心一看。在运行了一段时间后,我的笔记本电脑已经温热了许多。

粗暴的利用 top 命令一看:

f7e1e83dce77b60bfb0f62231ce488dc.png

例子中 Go 工程的内存占用竟然已经达到了 30+GB 之高,并且还在持续增长。在再等待了一段时间后(所设置的超时时间到达),Go 工程的内存占用也没有要恢复合理的数值。这非常可怕。

这明显就是存在内存泄露的问题。

问题原因

这个内存泄露的问题,无容置疑是 Go 官方认可的 BUG。

快速的用一句话来讲,核心原因在于:for select 已结束,无法被 GC,时间堆内的被触发的计时器还在。

d9a5e41f15a417594d4580302d13dea2.png
Go 官方文档说明

如果是想深入看原因可以查看以前我写的《Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!

Go1.23 timer.After 不泄露了!

在现在 2024 年,经过将近十年的努力,Go 核心团队负责人 rsc 终于解决了这个问题!!!

a239cefa12a71f7a5ce5f81964be2f06.png

自 Go1.23 版本起,会对用于计时器的通道(或者可能是用于通道的计时器)进行特殊处理,以便当没有通道操作待处理时,计时器将不会存放在计时器堆中。

这意味着当一旦不再引用通道和计时器,就可以对其进行 GC,不必等待计时器到期或明确停止计时器

注:这里的计时器是指 time.Aftertime.NewTimertime.NewTicker 使用的数据结构。

测试和验证

可能会有的同学会想体验 Go1.23 的新特性,验证这个 time.After 的修复是否有效。要特别注意下面这一点。

我们还是用前面提到的问题代码来测试。但如果你直接在本地复用,可能不一定能生效,会看到还是有内存泄露的情况。

主要是两个原因,如下:

1、你要下载 Go 新版本并使用 Go1.23 运行:

// 安装 go1.23rc2 的 go 新版本
$ go install golang.org/dl/go1.23rc2@latest
$ go1.23rc2 download

// 运行煎鱼前面的代码例子
$ go1.23rc2 run main.go

2、项目的 go.mod 文件注意 go 版本在 1.23,否则该新特性将由于兼容性保障无法生效:

e86d98425f434803cab95b0e4a5068c3.png

运行一段时间后,之前的代码中 Go1.23rc2 下内存情况基本正常:

42268cc339e21279df51c5c6fb1a6428.png

总结

今天给大家分享了一个花了将近 10 年,Go 才解决的计时器泄露问题。为此还是要给 rsc 点赞的,至少一直都有记着。就是这个解决速度比较慢,很多人在真实的 Go 工程中都已经遇到过了。

另外从新版本开始,大家在旧项目体验新特性是,要注意项目 go.mod 的 go 行版本或是 go toolchain 版本,避免由于版本过低而无法测试到真实的新特性效果。

推荐阅读

关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇

5d62ac6c00dfac998656b4cbc412d9a5.jpeg

985378965f2e380c3aa2cec9050d557c.png

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

原创不易 点赞支持

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值