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

大家好,我是煎鱼。

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

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

timer.After 是什么

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

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

go 复制代码
func After(d Duration) <-chan Time

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

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

go 复制代码
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 工程中经常能看见,只是大家都没怎么关注。

代码如下:

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 命令一看:

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

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

问题原因

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

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

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

Go1.23 timer.After 不泄露了!

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

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

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

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

测试和验证

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

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

主要是两个原因,如下:

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

go 复制代码
// 安装 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,否则该新特性将由于兼容性保障无法生效:

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

总结

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

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

  • 本文作者:煎鱼
  • 公众号:脑子进煎鱼了
  • 联系方式(微信号):cJY0728(加我拉你进技术交流群)

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo... 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

推荐阅读

相关推荐
uccs15 小时前
go 第三方库源码解读---go-errorlint
后端·go
bear_791 天前
Go操作MySQL
开发语言·go
我是前端小学生2 天前
Go语言中,函数参数是空接口的场景
go
慕城南风2 天前
Go语言中的defer,panic,recover 与错误处理
golang·go
桃园码工4 天前
1-Gin介绍与环境搭建 --[Gin 框架入门精讲与实战案例]
go·gin·环境搭建
云中谷4 天前
Golang 神器!go-decorator 一行注释搞定装饰器,v0.22版本发布
go·敏捷开发
苏三有春4 天前
五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)
git·go·github
我是前端小学生5 天前
Go语言中的方法和函数
go
探索云原生5 天前
在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析
ai·云原生·kubernetes·go·gpu
自在的LEE5 天前
当 Go 遇上 Windows:15.625ms 的时间更新困局
后端·kubernetes·go