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 催更。

推荐阅读

相关推荐
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
qq_172805593 天前
GO Govaluate
开发语言·后端·golang·go
littleschemer3 天前
Go缓存系统
缓存·go·cache·bigcache
程序者王大川4 天前
【GO开发】MacOS上搭建GO的基础环境-Hello World
开发语言·后端·macos·golang·go
Grassto4 天前
Gitlab 中几种不同的认证机制(Access Tokens,SSH Keys,Deploy Tokens,Deploy Keys)
go·ssh·gitlab·ci
高兴的才哥5 天前
kubevpn 教程
kubernetes·go·开发工具·telepresence·bridge to k8s
少林码僧6 天前
sqlx1.3.4版本的问题
go
蒙娜丽宁6 天前
Go语言结构体和元组全面解析
开发语言·后端·golang·go
蒙娜丽宁6 天前
深入解析Go语言的类型方法、接口与反射
java·开发语言·golang·go
三里清风_6 天前
Docker概述
运维·docker·容器·go