标签 Go 下的文章

Go 官方详解“Green Tea”垃圾回收器:从对象到页,一场应对现代硬件挑战的架构演进

本文永久链接 – https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc

大家好,我是Tony Bai。

关注 Go 语言演进的 Gopher 们可能已经注意到,Go 团队更换技术负责人以来,对运行时 (runtime) 和编译器 (compiler) 核心组件的打磨正日益成为团队的工作重心。从备受期待的“绿茶”GC (Green Tea GC),到 标准库simd 加速包的探索,再到 基于swisstable的 map 的实现,以及 json/v2 的设计实现,一系列动作都预示着 Go 正在其性能核心地带进行着深刻的自我革新。

而就在最近,Go 运行时和编译器团队的一项决议,更是将这一趋势推向了高潮:他们计划在 Go 1.26 版本中,将实验性的“绿茶”GC 作为默认的垃圾回收器正式落地

为了帮助大家深入理解这一重大变更背后的技术原理与深层思考,我翻译了 Go 官方博客10月29日的最新文章《The Green Tea Garbage Collector》。该文是基于 Go 团队核心成员 Michael Knyszek 在 GopherCon 2025 大会上的演讲整理而成。在这篇极具技术深度的原理文章中,没有人能比官方团队的讲解更为专业和权威。因此,为了最大程度地保留其“原汁原味”,我选择以全文翻译的形式,将其最真实、最精确的面貌呈现给大家。

以下是译文全文,供大家参考。


Go 1.25 包含一个名为“绿茶”(Green Tea)的全新实验性垃圾回收器,在构建时通过设置 GOEXPERIMENT=greenteagc 即可启用。使用该垃圾回收器后,许多工作负载在垃圾回收上花费的时间减少了约 10%,而有些工作负载的降幅甚至高达 40%!

它已为生产环境准备就绪,并在 Google 内部投入使用,因此我们鼓励你进行尝试。我们知道某些工作负载的收益不大,甚至完全没有,所以你的反馈对于我们向前推进至关重要。根据我们目前掌握的数据,我们计划在 Go 1.26 中将其设为默认GC

如需报告任何问题,请提交一个新 issue

如需分享任何成功经验,请回复至现有的 Green Tea issue

下文是基于 Michael Knyszek 在 GopherCon 2025 上的演讲整理的博文。一旦演讲视频上线,我们将会更新此博文并附上链接。

追踪垃圾回收过程

在讨论“绿茶”之前,让我们先就垃圾收集问题达成共识。

对象和指针

垃圾回收的目的是自动回收并重用程序不再使用的内存。

为此,Go 垃圾回收器关注的是对象(Object)和指针(Pointer)。

在 Go 运行时的上下文中,对象是Go值(Value),其底层内存分配自堆。当 Go 编译器无法找到其他方式为某个值分配内存时,就会创建堆对象。例如,以下代码片段会分配一个堆对象:一个指针切片的底层存储空间。

var x = make([]*int, 10) // 全局变量

Go 编译器只能在堆上分配切片后备存储,因为它很难(甚至可能不可能)知道 x 将引用该对象多长时间。

指针只是一些数字,用于指示 Go 值在内存中的位置,Go 程序通过它们来引用对象。例如,要获取上一个代码片段中分配的对象的起始指针,我们可以这样写:

&x[0] // 0xc000104000

标记-清除算法

Go 的垃圾回收器遵循一种广义上称为“追踪式垃圾回收”的策略,这意味着垃圾回收器会跟随或追踪程序中的指针,以识别程序仍在使用的对象。

更具体地说,Go 垃圾回收器实现了标记-清除(mark-sweep)算法。这比听起来要简单得多。 可以把对象和指针想象成计算机科学意义上的图:对象是节点,指针是边

标记-清除算法就在这个图上运行的,顾名思义,它分两个阶段进行。

在第一阶段,即标记阶段,它从一组明确定义的、称为“根(root)”的源边开始遍历对象图。可以将其理解为全局变量局部变量。然后,它将沿途找到的所有东西标记为已访问(visited),以避免循环。这类似于典型的图遍历算法,如深度优先或广度优先搜索。

接下来是清除阶段。在我们的图遍历中未被访问到的任何对象,都是程序未使用不可达(unreachable)的。我们称这种状态为不可达,因为通过语言的语义,正常的安全 Go 代码已无法再访问那块内存。为完成清除阶段,算法只需遍历所有未访问的节点,并将其内存标记为空闲,以便内存分配器可以重用它们。

就是这样?

你可能觉得我在这里把事情想得有点过于简单了。垃圾回收器经常被比作魔法和黑盒子 。你的说法也对了一部分,实际情况要复杂得多。

例如,实际上,这个算法会与你的常规 Go 代码并行执行。遍历一个不断变化的图会带来挑战。我们还对这个算法进行了并行化,这一点稍后会再次提及。

但请相信我,这些细节大多与核心算法无关。核心算法实际上只是一个简单的图泛洪(graph flood)操作。

图泛洪示例

我们来看一个例子。请浏览下面的幻灯片图片,跟随步骤操作。

这里我们有一个包含一些全局变量和 Go 堆的图示。让我们一步步来分析。

左边是我们的根。它们是全局变量 x 和 y。这将是我们图遍历的起点。根据左下角的图例,它们被标记为蓝色,表示它们当前在我们的工作列表上。

右边是我们的堆。目前,堆中的所有东西都是灰色的,因为我们还没有访问过任何部分。

每个矩形中代表一个对象。每个对象都标有其类型。这个特殊的对象是 T 类型的对象,其类型定义在左上角。它有一个指向子节点数组的指针和一些值。我们可以推断这是一种递归的树形数据结构。

除了 T 类型的对象,你还会注意到我们有包含 *T 的数组对象。这些数组对象由 T 类型对象的 “children” 字段指向。

矩形内的每个方块代表 8 字节的内存。带有点的方块是一个指针。如果它有箭头,那么它是一个指向某个其他对象的非空指针。

如果它没有对应的箭头,那么它就是一个空指针。

接下来,这些虚线矩形代表空闲空间,我称之为空闲“槽位(slot)”。我们可以在那里放置一个对象,但目前还没有。

你还会注意到对象被这些带标签的、虚线圆角矩形组合在一起。每一个都代表一个页(page):一块连续的内存块。这些页被标记为 A、B、C 和 D,我将以此来称呼它们。

在这个图中,每个对象都被分配到某个页面中。就像实际实现一样,这里的每个页面只包含特定大小的对象。这正是 Go 堆的组织方式。

页也是我们组织每个对象元数据的方式。这里你可以看到七个框,每个对应页 A 中的七个对象槽位之一。

每个框代表一位(bit)信息:我们之前是否见过这个对象。实际上,Go运行时就是通过这种方式来管理对象是否已被访问过的,这一点稍后会很重要。

细节讲了很多,感谢你跟读。这些稍后都会派上用场。现在,让我们看看图泛洪如何应用于这幅图。

我们首先从工作列表中取出一个根。我们将其标记为红色,表示它现在是活跃的。

沿着根指针,我们找到了一个 T 类型的对象,并将其添加到我们的工作列表。根据图例,我们将该对象绘制成蓝色,以表明它已在工作列表中。请注意,我们同时在右上角的元数据中设置了与此对象对应的“已见”位。

下一个根也同样处理。

现在我们处理完了所有的根,工作列表上还剩下两个对象。让我们从工作列表中取出一个对象。

我们现在要做的是遍历该对象的指针,以找到更多的对象。顺便说一下,我们称遍历一个对象的指针为“扫描”该对象。

我们找到了这个有效的数组对象…

… 并将其添加到我们的工作列表中。

从这里开始,我们递归地进行。

我们遍历数组的指针。

找到更多对象…

然后我们遍历数组对象引用的那些对象!

请注意,我们仍然需要遍历所有指针,即使它们是 nil。我们事先并不知道它们是否为空。

这个分支下还有一个对象…

现在我们到达了另一个分支,从我们早先从某个根找到的页 A 中的那个对象开始。

你可能注意到了我们工作列表的“后进先出”规则,这表明我们的工作列表是一个栈,因此我们的图遍历近似于深度优先。这是有意为之的,并反映了 Go 运行时中实际的图遍历算法。

让我们继续…

接下来我们找到了另一个数组对象…

并遍历它…

我们的工作列表上只剩最后一个对象了…

让我们扫描它…

标记阶段完成了!我们没有任何正在处理的工作,工作列表也空了。所有用黑色绘制的对象都是可达的,所有用灰色绘制的对象都是不可达的。让我们一次性清除所有不可达的对象。

我们已将那些对象转换为空闲槽位,准备好容纳新的对象。

问题所在

经过上面一番摸索,我认为我们已经掌握了 Go 垃圾回收器的实际工作原理。目前看来,这个过程运行良好,那么问题出在哪里呢?

事实证明,在某些程序中,执行这个特定算法会花费大量时间,而且几乎会给所有 Go 程序带来显著的开销。Go 程序将 20% 甚至更多的 CPU 时间用于垃圾回收的情况并不少见。

让我们来分析一下这些时间都花在了哪里。

垃圾回收成本

在宏观层面上,垃圾回收器的成本由两部分组成。一是运行频率,二是每次运行所做的工作量。将这两者相乘,就得到了垃圾回收的总成本。

Total GC cost = Number of GC cycles × Average cost per GC cycle

即 总 GC 成本 = GC 周期数 × 每个 GC 周期的平均成本

多年来,我们一直在研究这个等式中的这两个术语。要了解更多关于垃圾回收器运行频率的信息,请参阅 Michael 在 2022 年 GopherCon EU 大会上的关于内存限制的演讲Go 垃圾回收器的指南也对此主题进行了很多阐述,如果你想深入了解,值得一看。

但现在,我们只关注第二部分,即每个周期的成本。

多年来,我们不断研究 CPU Profile分析结果,试图提高性能,从中我们了解到 Go 的垃圾回收器有两大特点。

第一,大约 90% 的垃圾回收器成本都花在了标记上,只有大约 10% 是在清除。事实证明,清除比标记更容易优化,多年来 Go 已经拥有了一个非常高效的清除器。

第二,在那段用于标记的时间里,有相当大一部分(通常至少有 35%),都浪费在了访问堆内存上。这本身已经够糟糕了,更糟糕的是,它完全阻碍了现代 CPU 真正高速运行的关键机制。

“微架构灾难”

在这种情况下,“堵塞工作机制(gump up the works)”意味着什么?现代 CPU 的具体构造相当复杂,所以我们用一个类比来说明。

想象 CPU 在一条路上行驶,这条路就是你的程序。CPU 想要加速到很高的速度,为此它需要能看清前方的路,并且道路必须畅通。但图遍历算法对 CPU 来说,就像在城市街道里开车。CPU 看不到拐角后的情况,也无法预测接下来会发生什么。为了前进,它必须不断地减速、转弯、在红绿灯前停下、避开行人。你的引擎有多快几乎无关紧要,因为你根本没有机会真正跑起来。

让我们通过再次审视我们的例子来使这一点更具体。我在这里的堆上叠加了我们所走的路径。每个从左到右的箭头代表我们做的一段扫描工作,虚线箭头则显示了我们在不同扫描工作之间是如何跳转的。

上图展示了我们的图泛洪示例中,垃圾回收器在堆中执行的路径。

请注意,我们正在内存中到处跳转,在每个地方只做一点点工作。特别是,我们频繁地在页之间,以及页的不同部分之间跳转。

现代 CPU 做了大量的缓存。访问主内存可能比访问缓存中的内存慢上 100 倍。CPU 缓存中填充的是最近访问过的内存,以及与最近访问过的内存相邻的内存。但是,并不能保证两个相互指向的对象在内存中也彼此靠近。图泛洪算法并没有考虑到这一点。

补充一点:如果我们只是在等待从主内存中获取数据,情况可能还没那么糟。CPU 会异步地发出内存请求,所以即使是慢的请求也可以重叠,只要 CPU 能看得足够远。但在图遍历中,每一小段工作都是不可预测的,并且高度依赖于上一段工作,所以 CPU 被迫几乎在每一次独立的内存获取后都进行等待。

不幸的是,对我们来说,这个问题只会越来越严重。业界有句格言:“等两年,你的代码会变得更快。”

但 Go,作为一个依赖于标记-清除算法的垃圾回收语言,却面临着相反的风险。“等两年,你的代码会变得更慢。” 现代 CPU 硬件的趋势正在给垃圾回收器的性能带来新的挑战:

  • 非一致性内存访问 (Non-uniform memory access)。 首先,内存现在往往与 CPU 核心的子集相关联。其他 CPU 核心访问该内存的速度比前者慢。换句话说,主内存访问的成本取决于哪个 CPU 核心正在访问它 。这种成本是不一致的,因此我们称之为非一致内存访问,简称 NUMA。

  • 内存带宽减少 (Reduced memory bandwidth)。 每个 CPU 的可用内存带宽随着时间推移呈下降趋势。这意味着虽然我们拥有更多的 CPU 核心,但每个核心能够提交的数据量相对较少。 对主内存的请求导致未缓存的请求等待时间比以前更长。

  • 越来越多的 CPU 核心 (Ever more CPU cores)。 上面,我们看的是一个顺序的标记算法,但真正的垃圾回收器是并行执行此算法的。这在核心数量有限的情况下扩展得很好,但即使经过精心设计,用于扫描的共享对象队列也会成为一个瓶颈。

  • 现代硬件特性 (Modern hardware features)。 新硬件拥有像向量指令这样的酷炫功能,让我们能一次性操作大量数据。虽然这有可能大幅提升速度,但目前还不清楚如何才能实现这一点。因为标记工作包含很多不规则且通常是小块的工作。

绿茶(Green Tea)

最后,我们来看看绿茶算法,这是我们对标记扫描算法的一个新的尝试。绿茶算法的核心思想非常简单:

操作页面,而不是对象。

听起来很简单,对吧?然而,为了弄清楚如何安排对象图遍历的顺序以及我们需要跟踪哪些内容才能使其在实践中有效运作,我们做了大量的工作。

更具体地说,这意味着:

  • 我们不再扫描对象,而是扫描整个页。
  • 我们不再在工作列表上跟踪对象,而是跟踪整个页。
  • 我们最终(在一个扫描周期结束时)仍然需要标记对象,但我们会跟踪每个页面本地标记的对象,而不是跟踪整个堆中的标记对象。

绿茶示例

让我们通过再次审视我们的示例堆,来看看这在实践中意味着什么,但这次运行的是“绿茶”而不是直接的图泛洪。

和之前一样,请跟随带注释的幻灯片进行浏览。

这和之前的堆是一样的,但现在每个对象有两个比特的元数据而不是一个。同样,每个比特或框,对应于页中的一个对象槽位。总的来说,我们现在有 14 个比特对应于页 A 中的七个槽位。

顶部的比特代表和以前一样的东西:我们是否见过一个指向该对象的指针。我称之为“已见” (seen) 位。底部的比特集是新的。这些“已扫描” (scanned) 位跟踪我们是否已经扫描了该对象。

这块新的元数据是必需的,因为在“绿茶”中,工作列表跟踪的是页,而不是对象。我们仍然需要在某种程度上跟踪对象,这就是这些比特的目的。

我们和以前一样开始,从根开始遍历对象。

但这一次,我们不是把一个对象放到工作列表上,而是把整个页——在这里是页 A——放到工作列表上,通过将整个页用蓝色阴影表示。

我们找到的对象也是蓝色的,表示当我们从工作列表中取出这个页时,我们将需要查看那个对象。请注意,对象的蓝色调直接反映了页 A 中的元数据。其对应的“已见”位被设置,但其“已扫描”位没有。

我们跟随下一个根,找到另一个对象,再次将整个页——页 C——放到工作列表上,并设置该对象的“已见”位。

我们处理完根了,所以我们转向工作列表,并从工作列表中取出页 A。

通过“已见”和“已扫描”位,我们可以知道页 A 上有一个对象需要扫描。

我们扫描那个对象,跟随它的指针。结果,我们将页 B 添加到工作列表,因为页 A 中的第一个对象指向了页 B 中的一个对象。

我们处理完页 A 了。接下来我们从工作列表中取出页 C。

与页 A 类似,页 C 上有一个单独的对象需要扫描。

我们在页 B 中找到了一个指向另一个对象的指针。页 B 已经在工作列表上了,所以我们不需要向工作列表添加任何东西。我们只需为目标对象设置“已见”位。

现在轮到页 B 了。我们在页 B 上累积了两个待扫描的对象,我们可以按内存顺序,连续处理这两个对象!

我们遍历第一个对象的指针…

我们在页 A 中找到了一个指向一个对象的指针。页 A 之前在工作列表上,但此时不在了,所以我们把它放回工作列表。与原始的标记-清除算法不同,在原始算法中,任何给定的对象在整个标记阶段最多只会被添加到工作列表一次;而在“绿茶”中,一个给定的页在标记阶段可能会多次出现在工作列表上。

我们在扫描完第一个之后,立即扫描页中的第二个“已见”对象。

我们在页 A 中又找到了几个对象…

我们扫描完页 B 了,所以我们从工作列表中取出页 A。

这次我们只需要扫描三个对象,而不是四个,因为我们已经扫描过第一个对象了。我们通过查看“已见”和“已扫描”位之间的差异,来知道要扫描哪些对象。

我们将按顺序扫描这些对象。

我们完成了!工作列表上没有更多的页了,我们也没有正在处理的东西。请注意,现在元数据都很好地对齐了,因为所有可达的对象都既被“已见”又被“已扫描”。

你可能在我们的遍历过程中也注意到了,工作列表的顺序与图遍历有点不同。图遍历是“后进先出”或类似栈的顺序,而这里我们对工作列表上的页使用的是“先进先出”或类似队列的顺序。

这是有意为之的。当页在队列中等待时,我们让“已见”对象在每个页上累积,这样我们就可以一次性处理尽可能多的对象。这就是我们能一次性处理页 A 上那么多对象的原因。有时候,懒惰是一种美德。

最后,我们可以像以前一样,清除掉未访问的对象。

驶上高速公路

让我们回到我们开车的比喻。我们终于要上高速公路了吗?

让我们回顾一下之前的图泛洪图片。

原始图遍历在堆中穿行的路径需要 7 次独立的扫描。

我们到处跳跃,在不同的地方做着零碎的工作。“绿茶”所走的路径看起来非常不同。

“绿茶”所走的路径仅需要 4 次扫描。

相比之下,绿茶在 A 和 B 页面上从左到右的移动次数较少,但每次移动时间更长。 这些箭头越长越好,箭头堆积越多,这种效果就越强。这就是绿茶的魅力所在。

这也是我们驰骋高速公路的机会。

这一切都使得它与微架构更加契合。现在,我们可以更精确地扫描彼此靠近的对象,从而更有可能利用缓存并避免使用主内存。同样,每页的元数据也更有可能被缓存。跟踪页面而非对象意味着工作列表更小,而工作列表压力的降低意味着争用更少,CPU 停顿也更少。

说到高速公路,我们可以把我们比喻意义上的引擎开到以前从未开过的档位,因为现在我们可以使用向量硬件了!

向量加速

如果你对向量硬件只有粗浅的了解,可能会不明白我们在这里如何使用它。但除了常见的算术和三角运算之外,最新的向量硬件还支持两项对绿茶算法非常有用的功能:超宽寄存器和复杂的位运算。

大多数现代 x86 CPU 都支持 AVX-512 指令集,它拥有 512 位宽的向量寄存器。如此宽的寄存器足以在 CPU 上仅使用两个寄存器来存储整个页面的所有元数据,从而使 Green Tea 能够仅用几条直线指令就完成整个页面的扫描。向量硬件长期以来一直支持对整个向量寄存器进行基本的位运算,但从 AMD Zen 4 和 Intel Ice Lake 开始,它还支持一种新的位向量“瑞士军刀”指令,使得 Green Tea 扫描过程中的关键步骤能够在几个 CPU 周期内完成。这些改进共同作用,使我们能够大幅提升 Green Tea 的扫描循环速度。

对于之前的图泛洪来说,这根本不可能,因为我们需要在各种大小的对象之间来回扫描。有时只需要两条元数据,有时却需要一万条。向量硬件根本无法满足这种可预测性和规律性要求。

如果你想深入了解一些细节,请继续阅读!否则,请随时跳到下面的【评估】小节。

AVX-512 扫描内核

要了解 AVX-512 GC 扫描是什么样子,请看下面的图。


用于扫描的 AVX-512 矢量内核

这里面涉及的内容很多,我们可能光是解释它的运作原理就能写一整篇博客文章。现在,我们先从宏观层面来概括一下:

  1. 首先,我们获取页面的“已查看”和“已扫描”位。请记住,页面中的每个对象对应一位,并且页面中的所有对象大小相同。
  2. 接下来,我们比较这两个位集。它们的并集成为新的“扫描”位,而它们的差集则是“活动对象”位图,它告诉我们在本次页面扫描过程中(与之前的扫描相比)需要扫描哪些对象。
  3. 我们计算两个位图的差值并进行“扩展”,这样就不是每个对象占用一位,而是页面中的每个字(8 字节)占用一位。我们称之为“活动字”位图。例如,如果页面存储 6 个字(48 字节)的对象,则活动对象位图中的每位将被复制到活动字位图中的 6 位。如下所示:
0 0 1 1 ...  → 000000 000000 111111 111111 ...
  1. 接下来,我们获取页面的指针/标量位图。同样,这里的每一位都对应页面的一个字(8 字节),并告诉我们该字是否存储指针。这些数据由内存分配器管理。
  2. 现在,我们取指针/标量位图和活动字位图的交集。结果就是“活动指针位图”:该位图告诉我们尚未扫描的任何活动对象中包含的整个页面中每个指针的位置。

  3. 最后,我们可以遍历页面内存并收集所有指针。逻辑上,我们遍历活动指针位图中的每个置位,加载该字处的指针值,并将其写回缓冲区。该缓冲区稍后将用于标记已访问的对象并将页面添加到工作列表中。利用向量指令,我们只需几条指令即可一次处理 64 字节。

让这一切变快的部分原因是 VGF2P8AFFINEQB 指令,它是“Galios Field新指令” x86 扩展的一部分,也是我们上面提到的位操作“瑞士军刀”。它是真正的明星,因为它让我们能够非常高效地完成扫描内核中的第 (3) 步。它执行逐位的仿射变换,将向量中的每个字节本身视为一个 8 位的数学向量,并将其与一个 8×8 的比特矩阵相乘。这一切都是在Galios Field GF(2) 上完成的,这意味着乘法是AND,加法是XOR。这样做的好处是,我们可以为每个对象大小定义几个 8×8 的比特矩阵,来精确地执行我们需要的 1:n 比特扩展。

完整的汇编代码,请看这个文件。“扩展器”为每个大小类别使用不同的矩阵和不同的排列,所以它们在一个由代码生成器编写的单独文件中。除了扩展函数,代码量其实不多。大部分代码都被极大地简化了,因为我们可以在纯粹位于寄存器中的数据上执行大部分上述操作。而且,希望很快这段汇编代码将被 Go 代码所取代

感谢 Austin Clements 设计了这个过程。它非常酷,而且非常快!

评估

那么,这就是Green Tea的工作原理。它到底有多大帮助呢?

效果可能相当显著。即使不考虑向量增强,我们的基准测试套件也显示垃圾回收的 CPU 成本降低了 10% 到 40%。例如,如果应用程序 10% 的时间都花在了垃圾回收器上,那么根据工作负载的具体情况,整体 CPU 消耗将降低 1% 到 4%。垃圾回收 CPU 时间降低 10% 大致是典型的改进幅度。
(有关这些细节,请参阅 GitHub issue。)

我们在谷歌内部推广了绿茶,并且大规模推广后也看到了类似的效果。

我们仍在推出向量增强功能,但基准测试和早期结果表明,这将额外带来 10%的 GC CPU 降低。

虽然大多数工作负载都能在一定程度上受益,但也有一些工作负载不会受益。

Green Tea 算法基于这样的假设:我们可以一次性在单页上累积足够多的对象进行扫描,从而抵消累积过程的成本。如果堆结构非常规则(对象大小相同,且在对象图中的深度也相近),那么这个假设显然成立。但是,有些工作负载通常要求我们每次只能扫描一个对象。这可能比图泛洪更糟糕,因为我们可能在尝试累积对象到页面上的过程中,反而做了更多工作,最终却失败了。

Green Tea 算法针对仅包含单个待扫描对象的页面进行了特殊处理。这有助于减少性能回退,但并不能完全消除它们。

然而,要超越图泛洪算法,所需的单页累积数据量远比你想象的要少。这项研究的一个意外发现是,每次仅扫描页面 2% 的数据就能取得比图泛洪算法更好的性能。

可用性

“绿茶”已经在最近的 Go 1.25 版本中作为实验性功能提供,并且可以通过在构建时将环境变量 GOEXPERIMENT 设置为 greenteagc 来启用。这不包括前述的向量加速。

我们预计在 Go 1.26 中将“绿茶”作为默认的垃圾回收器,但你仍然可以通过 GOEXPERIMENT=nogreenteagc 在构建时选择退出。Go 1.26 还将在较新的 x86 硬件上增加向量加速,并根据我们收集的反馈包含一系列的调整和改进。

如果可以,我们鼓励你尝试使用 Go 的最新tip版本!如果你更喜欢使用 Go 1.25,我们也同样欢迎您的反馈。请参阅这个 GitHub 评论,其中包含一些关于我们感兴趣的诊断信息、如果你可以分享的话,以及首选的反馈渠道的细节。

旅程

在结束这篇博文之前,让我们花点时间谈谈我们走到今天的历程,以及这项技术背后的人的因素。

绿茶的核心理念看似简单,就像某个人灵光一闪的灵感火花。

但事实并非如此。“绿茶”是许多人多年来共同努力和构思的成果。Go 团队的多位成员都参与了构思,包括 Michael Pratt、Cherry Mui、David Chase 和 Keith Randall。当时在英特尔工作的 Yves Vandriessche 的微架构见解也对设计探索起到了至关重要的作用。为了使这个看似简单的理念得以实现,我们尝试了许多方法,也处理了许多细节问题。


时间线描绘了我们在达到今天这种状态之前,尝试过的一些类似想法

这个想法的萌芽可以追溯到2018年。有趣的是,团队里的每个人都认为最初的想法是别人提出的。

绿茶这个名字是在2024年得来的。当时,奥斯汀在日本四处寻觅咖啡馆,喝了无数抹茶,并由此构思出了早期版本的原型!这个原型证明了绿茶的核心理念是可行的。从此,我们便开始了绿茶的研发之路。

在 2025 年,随着 Michael 将绿茶项目实施并投入生产,其理念进一步发展和变化。

这需要大量的协作探索,因为绿茶算法不仅仅是一个算法,而是一个完整的设计空间。我们认为,单凭我们中的任何一个人都无法独自驾驭它。仅仅有想法是不够的,你还需要弄清楚细节并加以验证。现在我们已经做到了,终于可以开始迭代了。

“绿茶”的未来是光明的。

再次,请通过设置 GOEXPERIMENT=greenteagc 来尝试它,并让我们知道它的效果如何!我们对这项工作感到非常兴奋,并希望听到你的声音!


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


想系统学习Go,构建扎实的知识体系?

我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的Go语言入门宝典,即刻开启你的Go语言高效学习之旅!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

Rust 布道者Jon Gjengset深度访谈:在 AI 时代,我们该如何思考编程、职业与未来?

本文永久链接 – https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future

大家好,我是Tony Bai。

他是 MIT 的博士,Rust 社区的知名布道者,《Rust for Rustaceans》作者,前亚马逊首席工程师,现欧洲顶尖 AI 防务公司 Helsing 的首席工程师。Jon Gjengset 的履历,本身就是一部现代软件工程师的精英成长史。

在一场深度访谈中,Gjengset 以其一贯的冷静与深刻,系统性地阐述了他对 Rust 语言的哲学、AI 带来的冲击、工程师的职业发展,乃至在美欧之间做出的人生选择。这既是一场关于技术的对话,更是一次关于如何在日益复杂的软件世界中,保持清醒思考和持续成长的思想盛宴。

Rust 的“预先头疼”哲学

连续九年被评为 Stack Overflow“最受喜爱”的语言,但实际使用率却仍在缓慢爬坡——Rust 的这种“叫好不叫座”现象背后,隐藏着其核心的设计哲学。Gjengset 将其精辟地概括为:“Rust 让你预先头疼 (gives you the headache up front)。”

“你终究需要修复这些 bug。问题只在于,你愿意在编译时修复它们,还是在六个月后,当你的生产系统崩溃时再修复?”

这正是 Rust 与 Go、Java 等 GC 语言在开发者体验上的根本分歧。Rust 通过其著名的借用检查器 (Borrow Checker),在编译期强制开发者思考清楚数据的生命周期和所有权,以换取运行时的极致安全和性能。

这个陡峭的学习曲线,也正是 Rust 最大的“护城河”。Gjengset 认为,学习 Rust 的过程,本质上就是在你的大脑中,强制安装并训练一个强大的静态分析器。这个“脑内借用检查器”一旦形成,其价值将溢出到你使用的所有语言中。

AI 时代的“悲观”乐观主义

当被问及 AI 是否会取代程序员时,Gjengset 展现了一种独特的“悲观的乐观主义”。

  • 悲观之处:“AI 被过度炒作了,因为它无法真正理解”

他认为,当前由 LLM 驱动的 AI,其核心能力是模式复制与推断,而非真正的理解与推理 (understanding and reasoning)

“它们在预测行星的位置上表现出色,但它们无法推导出驱动其运动的底层物理原理。”

他将这一观点延伸到编程领域:AI 擅长编写那些有大量现有范例可供学习的代码,但对于那些需要深刻理解类型系统、并发模型或创造全新抽象的创新性任务,AI 依然力不从心。

  • 乐观之处:“它只是更好的电锯”

Gjengset 引用了一位开发者在 BlueSky 上的比喻,来阐述他对 AI 工具角色的看法:

“因为 Agentic AI 的出现而辞去软件工程师的工作,就像因为电锯的发明而辞去木匠的工作一样,毫无道理。”

AI 并非替代品,而是一个强大的工具,一个“加速器”。它将开发者从重复、繁琐的“模板式”工作中解放出来,让我们有更多时间去从事更高层次的、更具创造性的工作。

工程师的职业选择 —— 从 AWS 到欧洲独角兽

Gjengset 的职业路径,本身就是一部关于工程师如何在巨头与创业公司之间做出选择的生动教材。

  • 在亚马逊:自下而上的变革

在 AWS,他的职责是构建和维护 Rust 的内部构建系统。他强调,Rust 在亚马逊的普及,并非一次自上而下的行政命令,而是一场由一线团队驱动的、自下而上的变革。团队选择 Rust 的核心驱动力,是为了解决 Java/Kotlin 在 GC 停顿下难以优化的尾部延迟 (tail latency) 问题。

  • 离开美国,回归欧洲:一次关于“社会”的选择

2023 年,Gjengset 做出了一个令许多人意外的决定:离开美国,搬回欧洲。他坦言,这并非一个纯粹的职业选择,而是一个更深层次的、关于“社会” (society) 的选择。他的选择,为所有面临跨国职业机会的工程师提供了一个深刻的参考:职业选择,最终是个人价值观的体现。

对 Go 的犀利‘判词’——一场关于权衡的对话

Gjengset 的故事与 Go 有着不解之缘——他最初的博士论文项目原型,正是用 Go 编写的。这段经历,让他对 Go 与 Rust 的哲学差异,有了最为直观和深刻的体悟。

核心批评:“Go 忽略了自 70 年代以来的编程语言研究”

当被问及“Rust 在哪些方面比 Go 更好”时,Gjengset 的回答直截了当,甚至有些“刺耳”:

“哦,Rust 比 Go 更好,因为它有类型系统。这太简单了。Go 在被创造时,选择性地忽略了自 1970 年代以来几乎所有的编程语言研究成果。而 Rust 则决定从这些创新中学习。最终,你得到了一门更复杂,但写起来也有趣得多、表达力强得多的语言。对我来说,这就是最大的区别,也是我不想再用 Go 的原因。”

这句犀利的批评,直指 Go 语言设计的核心权衡:Go 为了追求极致的“简单”,在语言的“表达力”上做出了巨大的妥协。

Gjengset 认为,Rust 强大的类型系统(如 enum、模式匹配、Trait 系统)不仅仅是为了内存安全,更是为了让开发者能够在编译期,就对程序的行为建立起更强大的保证 (Guarantees)。他举例说,在 Rust 中可以利用类型系统创建 CoordinateInFrameA 这样的类型,从而在编译期就杜绝坐标系混用的错误,而这在 Go 中难以轻易实现。

Go 的“nil 指针” vs. Rust 的“编译期保证”

在向一个 Go 团队“推销”Rust 时,Gjengset 会说什么?

“你的应用在运行时因为一个错误而崩溃,这感觉很糟糕吧?在 Rust 中,这种事发生的概率要小得多。”

他认为,Go 开发者引以为傲的“我没有空指针异常”,其实只是将问题转化为了“nil 指针异常”。虽然 Go 通过 if err != nil 强制处理错误,但大量的业务逻辑错误,依然只能在运行时暴露。而 Rust 通过其类型系统和所有权模型,能将更多类别的错误扼杀在编译阶段。

“脑内借用检查器”对 Gopher 的价值

Gjengset 提出的一个极具启发性的观点是,学习 Rust 的思维模式,可以反哺我们的 Go 编程实践。一个内化了“借用检查”思想的 Gopher,会对以下问题更加敏感:

  • 理解 Go 的逃逸分析:当你的“脑内借用检查器”告诉你“从函数返回一个局部变量的引用是不合法的”时,在 Go 的世界里,这意味着“哦,这个变量会逃逸到堆上,我应该思考一下这带来的性能影响”。
  • 编写更健壮的并发代码:虽然 Go 的 channel 提供了强大的并发同步机制,但对于通过指针共享数据等场景,一个关于数据所有权和生命周期的清晰心智模型,能帮助你从根本上避免数据竞争。

小结:给开发者的忠告 —— 跨越语言的智慧

在访谈的最后,Gjengset 还分享了他对 C++ 等语言的看法,这些看法充满了辨证的智慧。

  • 对 C++ 团队:“你已经通过 RAII 获得了部分内存安全,但你的并发安全呢?Rust 可以在编译期静态地排除数据竞争。”
  • 对所有开发者:不要害怕借用检查器。它虽然让你“预先头疼”,但它正在你的大脑中构建一个关于数据流的强大心智模型,这个模型将使你在使用任何语言时,都成为一个更优秀的程序员。

Jon Gjengset 的这场访谈,远不止是一次对 Rust 的“布道”。它是一次关于工程权衡、技术信仰、职业战略和个人价值观的深度剖析。对于 Gopher 而言,这场来自 Rust 阵营的“他山之石”,深刻地揭示了 Go 在诞生之初所做出的核心权衡:用语言表达力的“舍”,换取工程效率和心智负担的“得”。

理解这场对话,将使我们对自己手中的工具,有更清醒的认知和更深刻的敬畏。

资料链接:https://www.youtube.com/watch?v=nOSxuaDgl3s


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


想系统学习Go,构建扎实的知识体系?

我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的Go语言入门宝典,即刻开启你的Go语言高效学习之旅!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats