为何要搭建本地 MCP 服务器(以及如何在 15 分钟内完成)






本文永久链接 – https://tonybai.com/2026/04/07/garbage-collectors-deep-dive
大家好,我是Tony Bai。
为什么 Java 的 G1GC 需要设置停顿目标?Go 的混合写屏障是如何消除栈重扫的?Python 又是如何解决引用计数无法处理的循环引用?
垃圾回收(GC)不仅是语言运行时的核心,更是理解高性能系统绕不开的坎。
本文翻译自Shubham Raizada的文章《Garbage Collection: From First Principles to Modern Collectors in Java, Go and Python》。
此文通过对历史经典论文的溯源和对现代主流语言底层实现的拆解,构建了一套完整的 GC 知识体系。
文章涵盖了从基础的标记-清除、复制与整理算法,到复杂的三色标记抽象、写屏障机制以及有色指针技术。
无论你是想调优 JVM 性能,还是试图理解 Go 并发垃圾收集的吞吐成本,这篇文章都将为你提供从理论支撑到代码实现的全景视角。

以下是译文全文:
在过去的几年里,我的技术栈经历了从 Java 到 Go,再到 Rust,现在又回到了 Java 的过程。
在这些语言之间切换时,一直绕不开的一个话题就是垃圾回收(Garbage Collection, GC)。Java 和 Go 有 GC,而 Rust 没有。
在基准测试、延迟讨论以及“为什么这个服务变慢了”的对话中,GC 总会出现在某个角落。我经常听到关于 GC pauses(GC 停顿)、throughput overhead(吞吐量开销)和 write barriers(写屏障)的讨论,但我并不完全理解底层发生了什么。
在追溯起源时,我读到了 McCarthy 1960 年的论文,这篇论文因引入 Lisp 而闻名,但它也是首次描述 mark-and-sweep(标记-清除)的地方。
这又引导我阅读了 Wilson 1992 年的综述《Uniprocessor Garbage Collection Techniques》,该文将随后的所有发展组织成了一个清晰的分类学。
阅读这两篇文献让我更容易理解现代垃圾收集器,因为 G1GC、ZGC、Go 的并发收集器以及 CPython 的混合方案全都是这些论文所描述思想的变体。我还用 Go 编写了一个简单的玩具级 GC,以便亲自观察其机制。
以下是我在这一过程中的笔记。
这篇论文因引入 Lisp 而闻名,但垃圾回收器几乎是作为实现细节被埋藏在其中的。McCarthy 需要一种方法来管理符号表达式的内存。Lisp 程序操作的是嵌套的列表(lists of lists of lists),这种递归结构使得要求程序员手动释放内存变得不切实际。因此,他描述了一种自动执行此操作的机制。
该机制分为两个阶段。首先,从程序正在活跃使用的 root(根)变量开始,遍历它们引用的每一个对象,将每个对象标记为 reachable(可达)。其次,扫描所有内存。任何未被标记的对象都是垃圾。将它们重新添加回 free list(空闲列表)。
这就是 mark-and-sweep(标记-清除)。它能自然地处理 cycles(循环引用,因为不可达的循环永远不会被标记),不需要逐个对象的簿记工作,并让程序员可以完全忽略内存管理。
其代价是程序在收集器运行时必须完全停止。每一次分配、每一次计算,所有一切都会冻结,直到标记和清除完成。对于 McCarthy 在 1960 年编写的程序来说,这完全是合理的。
随着程序规模变大并进入对延迟敏感的环境(如处理每秒数千次请求的 Web 服务器),stop-the-world(全线停顿)成了一个难以接受的权衡。现代 GC 研究产生的大部分成果都是为了回答一个问题:如何在不停止世界的情况下进行垃圾内存回收?
到 1992 年,三十年的 GC 研究已经产生了许多想法,但缺乏统一的词汇。Wilson 的综述论文将这一切组织了起来。它不是一种新算法,而是一个分类学,为散落在几十年论文中的思想赋予了名称和结构。
Wilson 正式确立了所有后续算法构建其上的三种经典算法。
第一种是 mark-and-sweep(标记-清除),即 McCarthy 的原始算法。从 roots 开始,遍历对象图,标记你能触达的所有内容,然后扫过堆并释放任何未标记的内容。它自然处理循环引用,实现简单。缺点是经过足够多的分配和回收循环后,堆会变得 fragmented(碎片化)。存活对象最终散落在各处,中间夹杂着细小的空闲间隙,分配器(allocator)必须更费力地寻找空间。
第二种是 copying(复制算法),有时被称为 semi-space(半空间)。其想法是将堆分成两个相等的部分。你在其中一半进行分配,当它填满时,将所有存活对象拷贝到另一半,然后将第一半完全丢弃。碎片消失了,因为存活对象在拷贝过程中被紧密排列在一起。分配速度很快,因为你只需移动一个 bump pointer(碰撞指针)。代价是有一半的内存始终处于空闲状态,等待成为下一次拷贝的目标。
第三种是 reference counting(引用计数)。每个对象都记录有多少个指针指向它。当创建一个新引用时,计数增加;当移除一个引用时,计数减少。当计数归零时,对象立即被释放。没有追踪过程,没有停顿,销毁是确定性的。问题在于 cycles(循环引用)。如果两个对象相互指向,即使程序中没有任何其他部分可以触达它们,它们的计数也至少为 1。仅靠引用计数,它们永远不会被释放。
除了这三种算法,Wilson 还探讨了现代垃圾回收器赖以生存的两个观察结果。
第一个是 generational hypothesis(分代假说):大多数对象死得早。在实践中,程序分配的临时对象(中间值、请求作用域的缓冲区、循环变量)往往很快变成垃圾,而只有一小部分对象会贯穿整个程序生命周期。如果你频繁回收年轻对象,偶尔回收老对象,你就能将大部分工作集中在堆中主要是垃圾的部分,这比每次都扫描所有内容的代价要小得多。
第二个是 tricolor marking(三色标记),这是一种用于增量和并发收集的抽象。你不再简单地将对象标记为已访问或未访问,而是使用三种颜色:white(白色,尚未见到)、grey(灰色,已见到但子节点尚未扫描)和 black(黑色,已完全处理)。收集器一次处理一个灰色对象。结束时,白色对象即为垃圾。这种抽象使得收集器和应用程序可以同时运行,而不会破坏彼此对堆的视图。Go 的并发 mark-and-sweep 和 ZGC 的并发标记都是这一思想的直接后裔。
本文“现代 GC”部分中的所有内容都可以映射回 Wilson 的分类。工程实现已经变得更加复杂,但底层结构依然如故。
几乎所有的垃圾回收器要么是 reference counting(引用计数),要么是 tracing(追踪),或者是两者的某种结合。Wilson 的论文围绕这一划分进行组织,三十年后依然成立。
每个对象维护一个指向它的引用计数。当引用创建时,计数增加。当引用移除时,计数减少。当计数归零时,对象立即被释放。

这是 CPython 所使用的其主要机制。它很简单,并能提供确定性的销毁。当指向文件句柄的最后一个引用消失时,del 运行,文件当场关闭,而不是在以后的某个 GC cycle中。
有两个问题使得引用计数无法独立胜任。
Cycles (循环引用)。 如果对象 A 指向对象 B,且对象 B 指向 A,那么即使程序中没有任何其他部分能触达它们,两者的计数也至少为 1。两者都不会被释放。

这并非理论上的边缘案例。循环引用在链表数据结构、父子关系、观察者模式和缓存中自然出现。稍后在介绍 CPython 的 GC 时,我将讨论 Python 如何处理这个问题。
Per-mutation overhead (每次修改的开销)。 每次指针赋值都需要更新引用计数。在多线程程序中,这些必须是 atomic(原子)操作,成本昂贵得多。每当你将对象传递给函数、返回它或将其赋值给字段时,你都要支付这种代价。
追踪式收集器不跟踪单个引用,而是从一组已知的存活引用(称为 root set,根集合)开始,遍历整个对象图。它能触达的每个对象都被标记为存活。其他所有对象都被释放。
Root set 是起点,因此什么算作 root(根)至关重要。不同语言的答案是相同的:root 是 runtime(运行时)无需追踪就能找到的任何引用。这些指针锚定在程序当前的执行状态中,是在任何遍历开始之前你就知道是存活的东西。
在实践中,roots 分为以下几类。
每个活跃 stack frame(栈帧)中的 local variables(局部变量)和函数参数都是 roots。程序正在活跃地运行这些函数,因此它们引用的任何内容定义上都是在使用中的。
Global and static variables(全局变量和静态变量)是 roots,因为它们在程序的整个生命周期内都存在。
CPU registers(CPU 寄存器)是 roots。因为当 JIT 编译器优化一个热点方法时,它可能会将频繁访问的对象引用保留在 CPU 寄存器中,而不是写回栈。如果 GC 此时运行,寄存器保存着该对象的唯一存活引用。如果 GC 不扫描寄存器,它就会释放一个仍在使用中的对象。为了防止这种情况,运行时在代码中定义了 safe points(安全点),GC 只能在这些点发生,并且在这些点,它会快照寄存器状态以寻找持有的任何引用。
Runtime(运行时)本身也持有与用户代码无关的 roots。在 JVM 中,class loaders 是 roots:你加载的每个类都由其类加载器引用,只要类加载器存活,它加载的每个类(包括它们的静态字段)就保持存活。Interned strings(常量池字符串)是 roots,因为 String.intern() 将字符串存储在 JVM 维护的共享池中。JNI handles 是 roots,因为当原生 C 或 C++ 代码通过 Java Native Interface 持有 Java 对象的引用时,该引用存在于 Java 堆外的句柄表中,GC 必须扫描它。每个活跃线程都是一个 root,其整个调用栈帧都是 root set 的一部分。
Go 的运行时遵循同样的原则。每个 goroutine 都有自己的栈,必须扫描所有 goroutine 栈以寻找 roots。运行时还跟踪自己的内部数据结构,例如 finalizer 队列,作为 root set 的一部分。

核心见解是:roots 是由运行时在无需追踪的情况下就已经知道是存活的东西定义的。其他所有东西必须通过从 root 可达来证明自己的生存权。这就是为什么这个概念是与语言无关的。Java、Go 和 Python 之间的具体 roots 集合有所不同,但原则是一样的:从你知道是存活的地方开始,向外追踪,并回收其余部分。
循环引用被自然处理。如果 A 和 B 相互指向,但都无法从任何 root 到达,则标记阶段永远不会访问它们。它们保持未标记状态并被清除。
代价:朴素的 mark-and-sweep 必须在追踪堆时暂停整个程序。这种 stop-the-world(全线停顿)是早期垃圾回收器的核心问题,也是现代 GC 几十年来工程化改进的重点。
在具有高分配速率的服务器工作负载中,引用计数的逐次修改成本会积少成多。每次指针写入都会增减计数。在多线程程序中,这些更新必须是原子的,而原子操作很昂贵。在数十个线程中每秒进行数千次分配时,这种开销变得可衡量。此外,循环引用问题无论如何都需要一个补充的追踪步骤。而且追踪式收集器可以做成并发的,在应用程序运行的同时运行,只有简短的停顿。
Java 和 Go 使用追踪式收集器。Python 是一个显著的例外,它以引用计数为基础,并在此之上增加了一层用于追踪循环引用的检测器。
Wilson 的论文描述了实现追踪的四种方式,每种方式都有不同的权衡。
最简单的追踪式收集器。分为两个阶段:

Mark-sweep 的主要问题是 fragmentation(碎片化)。经过足够的回收周期后,堆看起来就像瑞士奶酪:存活对象散布其间,中间有很小的空闲间隙。你总共可能有 100MB 空闲内存,但没有一个连续的块大到足以满足一次新分配。分配器必须维护一个 free list 并搜索合适的空间,随着堆变得碎片化,这会变慢。
堆被分成两个相等的一半:from-space(源空间)和 to-space(目标空间)。分配发生在 from-space,使用简单的 bump pointer(碰撞指针)。当 from-space 填满时,收集器将所有存活对象拷贝到 to-space,更新所有指针,然后交换两者的角色。旧的 from-space 被完全丢弃。

分配速度极快,因为它只是一个指针移动。Compaction(压缩)自然发生。代价是任何时候只有一半的堆可用。
标记阶段与 mark-sweep 相同,但收集器不是简单地释放未标记的对象,而是将所有存活对象滑动到堆的一端。这消除了碎片,且没有复制算法 50% 的内存开销。

缺点是整理需要对堆进行多次扫描:一次标记,一次计算新地址,一次更新所有指针,一次移动对象。
Wilson 论文中最具影响力的观察之一是弱分代假说:大多数对象死得早。
在典型的 Web 服务器中,每个请求都会创建临时对象(解析器、中间字符串、响应构建器),它们只存活几毫秒。配置对象、连接池和缓存则贯穿整个应用程序生命周期。
分代收集器利用这一点,将堆划分为 generations(代)。新对象进入 young generation(年轻代)。如果它们在几次回收中幸存下来,就会被提升到 old generation(老年代)。年轻代回收频繁且速度快,因为那里的大多数对象已经死了。老年代回收较少发生。

Eden 是所有新对象出生的地方。每一个 new Object() 都去这里。它很快就会填满,因为大多数程序分配速率很高。
S0 和 S1 是两个较小的 survivor spaces(幸存者空间)。当 Eden 填满并运行 minor GC(次要回收)时,收集器将 Eden 中的每个存活对象拷贝到其中一个空间(比如 S0)。下一次回收时,来自 Eden 和 S0 的幸存者被拷贝到 S1。再下一次,回到 S0。它们在每个周期轮换。这是年轻代中的复制算法:没有碎片,没有空闲列表,只有两半空间轮流充当目标。代价是你需要两个幸存者空间,但它们保持得很小,因为到回收运行时,Eden 中的大多数对象都已经死了。
Promotion to old generation (提升到老年代)。 在对象在 S0 和 S1 之间反弹足够多次之后(JVM 中的默认阈值是 15 次),收集器认定它已赢得了一席之地,并将其提升到老年代。老年代回收频率低得多,并且使用更重的算法(标记-整理而非复制),因为那里的对象庞大且长寿。
关键的实现挑战是跟踪从老对象到新对象的引用。如果一个老对象指向一个年轻对象,即使没有年轻代 root 指向它,该年轻对象也绝不能被回收。这通过 write barrier(写屏障)解决,即在每次指针写入时注入的一小段代码,用于在 remembered set(记录集)中记录跨代引用。
我写了一个极简的 mark-and-sweep 收集器来使这些概念具体化。它大约有 70 行代码,演示了完整循环:分配对象、构建对象图、从 roots 标记以及清除不可达对象。
package main
import "fmt"
// Object 代表一个在堆上分配的对象。
type Object struct {
name string
marked bool
children []*Object
}
// VM 是一个带有垃圾回收器的微型虚拟机。
type VM struct {
heap []*Object
roots []*Object // 模拟栈变量和全局变量
}
// NewObject 在 VM 的堆上分配一个对象。
func (vm *VM) NewObject(name string) *Object {
obj := &Object{name: name}
vm.heap = append(vm.heap, obj)
return obj
}
// mark 从每个 root 开始遍历并标记所有可达对象。
func (vm *VM) mark() {
for _, root := range vm.roots {
vm.markObject(root)
}
}
func (vm *VM) markObject(obj *Object) {
if obj == nil || obj.marked {
return
}
obj.marked = true
for _, child := range obj.children {
vm.markObject(child)
}
}
// sweep 释放未标记的对象并重置幸存者的标记。
func (vm *VM) sweep() {
alive := []*Object{}
for _, obj := range vm.heap {
if obj.marked {
obj.marked = false // 为下一个 GC 周期重置
alive = append(alive, obj)
} else {
fmt.Printf(" collected: %s\n", obj.name)
}
}
vm.heap = alive
}
// GC 运行一次完整的 mark-and-sweep 回收。
func (vm *VM) GC() {
fmt.Printf("gc: heap has %d objects\n", len(vm.heap))
vm.mark()
vm.sweep()
fmt.Printf("gc: %d objects remain\n\n", len(vm.heap))
}
func main() {
vm := &VM{}
a := vm.NewObject("A")
b := vm.NewObject("B")
c := vm.NewObject("C")
_ = vm.NewObject("D") // 已分配但从未链接到任何东西
// 构建图: A -> B -> C
a.children = append(a.children, b)
b.children = append(b.children, c)
// 只有 A 是 root
vm.roots = append(vm.roots, a)
fmt.Println("=== GC #1: D is unreachable ===")
vm.GC()
// 创建循环: C -> A, 然后移除所有 roots
c.children = append(c.children, a)
vm.roots = nil
fmt.Println("=== GC #2: A->B->C->A cycle, no roots ===")
vm.GC()
}
运行结果:
=== GC #1: D is unreachable ===
gc: heap has 4 objects
collected: D
gc: 3 objects remain
=== GC #2: A->B->C->A cycle, no roots ===
gc: heap has 3 objects
collected: A
collected: B
collected: C
gc: 0 objects remain
第一次回收:A、B 和 C 通过 root A 可达。D 没有任何 root 路径,因此被回收。
第二次回收:A、B 和 C 形成了一个循环(A->B->C->A),但没有 roots。标记阶段从未访问过它们中的任何一个。所有三个都被清除了。这正是击败引用计数的场景。循环中的每个对象都有非零的引用计数,但没有一个能从 root 到达。
追踪式 GC 不关心循环。它们只关心从 roots 开始的可达性。
有一点需要注意:markObject 函数使用了递归,这在深层对象图上会耗尽栈空间。真实的垃圾回收器使用显式的 worklist(工作列表)而不是调用栈。
上面的玩具收集器为了整个标记和清除过程停止了世界。现代 GC 已经进化到在应用程序持续运行的同时并发完成大部分工作。
Go 的垃圾回收器是非分代的、非整理的且并发的。它不按年龄区分对象,也不在内存中移动对象。其重点是保持低停顿时间。
收集器使用三色抽象(tri-color abstraction)进行并发标记。每个对象处于三种状态之一:

收集器开始时将所有对象设为白色,然后将 roots 设为灰色,并处理灰色对象直到不再剩余。所有仍为白色的内容都被清除。

开始: 所有对象为白色,roots 为灰色
步骤 1: 选取一个灰色对象,扫描其子节点
- 将子节点标为灰色
- 将扫描过的对象标为黑色
步骤 2: 重复直到没有灰色对象剩余
步骤 3: 所有白色对象都是垃圾
示例:
Roots: [A]
开始: A(grey) --> B(white) --> D(white)
A(grey) --> C(white)
扫描 A: A(black) --> B(grey) --> D(white)
A(black) --> C(grey)
扫描 B: A(black) --> B(black) --> D(grey)
A(black) --> C(grey)
扫描 C: A(black) --> B(black) --> D(grey)
A(black) --> C(black)
扫描 D: A(black) --> B(black) --> D(black)
A(black) --> C(black)
结果: 任何剩余的白色对象都是垃圾并被释放
难点在于应用程序在收集器遍历时持续运行并修改指针。这造成了一个需要仔细处理的正确性问题。
收集器认为黑色对象已完成。一旦对象变黑,收集器就不会再扫描它。它的所有子节点都已被访问并设为灰色。但是,如果应用程序在收集器仍在运行时,将一个指向白色对象的指针写入黑色对象,收集器就有麻烦了。黑色对象已经处理完了。该白色对象也无法从任何灰色对象触达。当标记阶段结束并清除运行时,该白色对象将被释放,即便有一个存活的黑色对象指向它。
这被称为 tricolor invariant(三色不变性):黑色对象绝不能直接指向白色对象。如果发生了这种情况,白色对象对收集器是不可见的,会被错误释放。write barrier(写屏障)的存在专门用于在并发标记期间应用程序修改对象图时维护这一不变性。
Go 通过 hybrid write barrier(混合写屏障,Go 1.8 引入)解决了这个问题。要理解它为什么有效,看看它结合的两种旧屏障会有所帮助。
Dijkstra’s 插入屏障 (1978):每当一个指针被写入对象时,将新的被引用者设为灰色。如果一个黑色对象存储了对白色对象的引用,该白色对象会在收集器错过它之前变灰。这维护了三色不变性。
问题在于 goroutine 栈与堆对象不同。编译器在堆指针写入处注入写屏障,例如写入结构体字段或切片元素。栈写入是局部变量赋值,编译器对其分别处理。在每一个局部变量赋值上放屏障会使函数调用和基本操作变得极其昂贵,所以屏障不覆盖它们。这意味着在并发标记期间,goroutine 可以自由地将指向白色对象的指针写入局部变量,而没有屏障触发。收集器不知道发生了这事。
为了修复这一点,在并发标记结束时,Go 曾经必须停止世界并从头重新扫描每个 goroutine 的整个栈。重新扫描时发现的任何指向白色对象的指针都会变灰,防止它们被错误释放。此步骤的停顿时间随着 goroutine 数量和其栈大小而增加。拥有成千上万个 goroutine 的程序可能会看到数毫秒的 STW 停顿,仅仅是为了这次重新扫描。这是 Go 1.8 之前主要的 STW 停顿来源。
Yuasa’s 删除屏障 (1990) 采取相反的方法:每当一个指针即将被覆盖时,在旧引用消失前将其变灰。这确保了在标记开始时可达的任何东西直到结束都保持可达,即便应用程序在标记期间丢弃了它的引用。缺点是标记期间死亡的一些对象会存活到下一个周期(floating garbage,浮动垃圾),因为屏障保守地让它们活着。
Go 的混合屏障结合了两者。在堆写入时,它同时应用两种屏障:将旧引用变灰(Yuasa)并将新引用变灰(Dijkstra)。在栈写入时,不运行屏障,但栈上新分配的对象开始时就是黑色而不是白色。这种组合赋予了收集器足够强的不变性,使其在标记结束时永远不需要重新扫描栈。STW 停顿从几十毫秒降到了不到一毫秒。
// 混合屏障在堆指针写入时的逻辑:
// *slot = new_ptr
shade(*slot) // 将旧引用变灰 (Yuasa: 不要丢掉之前在那里的内容)
shade(new_ptr) // 将新引用变灰 (Dijkstra: 不要错过新到来的内容)
*slot = new_ptr
这就是并发垃圾回收的吞吐量成本:标记阶段的每一次堆指针写入都要运行此 shade 逻辑。单次操作开销虽小,但在高分配速率下会累积。权衡的结果是你获得了亚毫秒级的 STW 停顿,而不是几十毫秒。
Go 仅简短地停止世界以扫描 goroutine 栈并切换写屏障的开关。实际的标记和清除与应用程序并发进行。
No compaction (无整理)。 Go 在分配后不移动对象。相反,Go 使用 tcmalloc 风格的分配器,将内存划分为 size classes(大小类),并从每个处理器的缓存(per-processor caches)中分配。对象被分组为固定的大小类(8 字节、16 字节、32 字节,最高达 32 KB)。分配时从空闲列表中选取合适大小的槽。这减少了碎片而无需移动对象,但并不能完全消除碎片。
No generational collection (无分代收集)。 Go 团队的理由是,考虑到 Go 典型的带有 goroutine 和并发工作负载的分配模式,分代 GC 增加的复杂性(用于跟踪老到新指针的写屏障、提升逻辑、分代大小调优)带来的收益是不确定的。Go 通过使其并发标记器足够快来补偿,从而使额外的回收频率变得可以接受。
关键里程碑:
GOGC 调节旋钮。 Go 提供了一个主要的调优参数:GOGC。它控制在下一次 GC 触发之前堆可以增长多少。默认值是 100,意味着当堆自上次回收以来翻倍时触发 GC。

GOGC=100 (默认):
GC 后,存活堆 = 500MB
下次 GC 触发点: 500MB * (1 + 100/100) = 1000MB
GOGC=50 (更激进):
GC 后,存活堆 = 500MB
下次 GC 触发点: 500MB * (1 + 50/100) = 750MB
GOGC=200 (较保守):
GC 后,存活堆 = 500MB
下次 GC 触发点: 500MB * (1 + 200/100) = 1500MB
更低的 GOGC 意味着更频繁的回收(更低的内存占用,更高的 CPU 开销)。更高的 GOGC 意味着较少的回收(更高的内存占用,更低的 CPU 开销)。
Go 1.19 增加了 GOMEMLIMIT,这是一个软内存限制。在具有硬性内存预算的容器环境中,GOMEMLIMIT 告诉 GC pacer(步调算法)在内存使用接近限制时变得更加激进。
亲自尝试:
package main
import (
"fmt"
"runtime"
"time"
)
var longLived []*[1024 * 1024]byte
func main() {
fmt.Println("Go version:", runtime.Version())
for round := 0; round < 50; round++ {
// 短寿对象: 分配小对象,让它们死亡
for i := 0; i < 5000; i++ {
_ = make([]byte, 1024)
}
// 长寿对象: 每 10 轮保留一个
if round%10 == 0 {
arr := new([1024 * 1024]byte)
longLived = append(longLived, arr)
}
time.Sleep(50 * time.Millisecond)
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Total GC cycles: %d\n", stats.NumGC)
fmt.Printf("Total STW pause: %v\n", time.Duration(stats.PauseTotalNs))
fmt.Printf("Long-lived objects: %d\n", len(longLived))
}
运行并开启 GC 追踪:
GODEBUG=gctrace=1 go run gcdemo.go
观察输出内容:
gc 1 @0.011s 1%: 0.044+0.56+0.13 ms clock, 0.62+0.21/0.57/0+1.8 ms cpu, 3->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 14 P
从左到右阅读:
0.044+0.56+0.13 ms clock: GC 周期的三个阶段:STW 标记开始 (0.044ms) + 并发标记和扫描 (0.56ms) + STW 标记结束 (0.13ms)。STW 停顿是 clock 字段中的第一个和第三个数字。在此例中,应用程序被冻结的总墙钟时间是 0.044 + 0.13 = 0.174ms。中间的 0.56ms 是并发的:你的应用程序一直在运行。在 Go 中,STW 停顿通常在 1ms 以下,往往远低于 0.1ms。
0.62+0.21/0.57/0+1.8 ms cpu: CPU 时间细目。格式为:STW-开始 + 辅助/背景/空闲 + STW-结束。每个数字代表:

当多个核心并行工作时,CPU 时间可以超过墙钟时间。并发阶段的 CPU 时间可能少于墙钟时间,因为 GC 与你的应用程序共享核心。
G1GC 自 JDK 9 以来一直是 Java 的默认垃圾回收器。它是一个分代的、基于区域(region)的收集器。它进行追踪、标记和整理,但它是增量式进行的,而不是一次性完成。
Region layout (区域布局)。 G1 将堆划分为大小相等的区域,通常每个区域为 1MB 到 32MB,取决于堆的大小。每个区域在任何时候扮演四种角色之一:Eden(伊甸园)、Survivor(幸存者)、Old(老年代)或 Humongous(巨型对象,用于超过半个区域大小的对象)。区域的角色可以在不同回收周期之间改变。

Young collection (次要 GC)。 Eden 区域填满。G1 停止世界,使用并行多线程标记器标记 Eden 和 Survivor 区域中的存活对象,将幸存者拷贝到新的 Survivor 区域或提升到 Old 区域,并完全丢弃旧的 Eden 区域。这是一个并行的 STW 停顿,但很短,因为年轻代区域较小且年轻对象大多已死。
Mixed collection (混合回收)。 G1 周期性地运行并发标记周期,以找出哪些老年代区域包含的垃圾最多。然后运行混合回收:同时疏散(evacuating)年轻代区域和最具“盈利价值”的老年代区域。这就是“Garbage First”名称的由来。G1 总是优先选取垃圾密度最高的老年代区域,从而在单位停顿时间内实现最大的回收量。
SATB (Snapshot-At-The-Beginning,起始快照)。 在并发标记期间,应用程序持续运行并修改对象图。G1 使用 SATB 维护正确性。在标记开始时,G1 对哪些对象存活进行逻辑快照。该快照中存活的对象在此周期被视为存活,即使应用程序在标记期间丢弃了它们。写屏障将修改字段的旧值记录到 SATB 队列中。这种做法是保守的(一些垃圾会存活到下个周期),但是正确的。
并发标记正在运行。应用程序执行:
obj.field = null (原本指向 X)
没有 SATB: X 可能没有其他引用,未被标记,在使用中被释放。
有 SATB: 写屏障记录“此处曾有 X”,将 X 标为灰色。安全。
Pause target (停顿目标)。 你可以通过 -XX:MaxGCPauseMillis 配置 G1 的目标最大停顿时间。默认值是 200ms。G1 通过调整区域数量、回收集合大小和时机,尝试将停顿保持在目标范围内。它并不总是能成功,特别是在 Full GC 期间,但它是主要的调优旋钮。
亲自尝试:
import java.util.ArrayList;
import java.util.List;
public class GCDemo {
static List<byte[]> longLived = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
System.out.println("Starting GC demo...");
for (int round = 0; round < 50; round++) {
// 短寿对象:创建并立即丢弃
for (int i = 0; i < 1000; i++) {
byte[] tmp = new byte[10 * 1024]; // 每个 10KB
}
// 长寿对象:保留一些对象以构建老年代
if (round % 5 == 0) {
longLived.add(new byte[1024 * 1024]); // 1MB
}
Thread.sleep(50);
}
System.out.println("Done. Long-lived objects: " + longLived.size());
}
}
使用 G1GC 日志运行:
# 编译
javac GCDemo.java
# 使用 G1GC (Java 9+ 默认) 并开启 GC 日志
java -Xmx256m \
-XX:+UseG1GC \
"-Xlog:gc*:file=gc_g1.log:time,uptime,level,tags" \
GCDemo
# 或者,使用简洁的一行输出
java -Xmx256m -Xlog:gc GCDemo
观察日志:
[0.005s][info][gc] Using G1
[0.135s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 26M->3M(256M) 0.644ms
[0.812s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 132M->7M(256M) 0.707ms
[1.710s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 165M->13M(256M) 1.019ms
[2.528s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 171M->19M(256M) 0.964ms
阅读日志:
ZGC 自 Java 11 起可用,并在 Java 15 中达到生产就绪状态。扩展了分代收集的 Generational ZGC 在 Java 21 中引入。ZGC 的目标是无论堆大小如何(包括数百 GB 的堆),停顿时间均保持在亚毫秒级。
G1 在年轻代回收时停顿较短,但随着堆的增长,在并发标记设置和混合回收期间会有更长的停顿。ZGC 的方法不同:它几乎将所有工作(标记、重定位、引用处理)并发进行,将 STW 工作降至最低。
Colored pointers (有色指针)。 ZGC 直接在指针位中编码 GC 元数据。在 64 位平台上,指针宽度为 64 位,但你实际上并不需要所有 64 位来寻址内存。2^42 就能给你 4TB 的可寻址空间,这超出了大多数应用程序的使用范围。这使得每个指针中留有超过 20 位空闲。ZGC 重新利用其中一些空闲位,直接在指针内部存储 GC 状态。

每个元数据位都有特定用途:
M0 和 M1 (标记位): 用于跟踪对象是否已被标记为存活。ZGC 在每个 GC 周期中交替使用 M0 和 M1。在周期 1,收集器对每个可达对象设置 M0。在周期 2,它改用 M1。这样收集器就能区分“本周期标记”和“上个周期标记”,而无需在周期之间清除所有标记位。
Remap (R,重映射): 此位跟踪在对象重定位(relocated)后指针是否已更新。在并发重定位期间,ZGC 将对象移动到新地址,但并不立即更新堆中的每一个指针。相反,它保留旧指针,并使 remap 位处于未设置状态。当应用程序加载这些过时指针之一时,load barrier(读屏障/加载屏障)会注意到 remap 位未设置,并对其进行修正。
Finalizable (F): 表示该对象具有一个需要在释放前运行的 finalizer。
巧妙之处在于元数据随指针移动。GC 不需要一个单独的侧表来查找对象的 GC 状态。每个指针都已经携带了它。
Load barriers (加载屏障)。 每次应用程序从堆加载引用时,ZGC 都会插入一个加载屏障。屏障检查指针的颜色位,如果它们不处于预期状态,则采取行动。
以下是实际操作中的情况。假设收集器在并发重定位阶段将一个对象从地址 0×1000 移动到了 0×2000。应用程序仍然持有一个地址为 0×1000 且 remap 位未设置的指针。
应用程序代码:
Object x = obj.field;
实际执行的内容:
raw_ptr = load obj.field // raw_ptr = 0x1000, remap bit = 0
if (raw_ptr.color != expected) { // remap bit 为 0, expected 为 1 → 进入 slow path
new_addr = forwarding_table[0x1000] // 查找: 对象已移动到 0x2000
raw_ptr = set_address(raw_ptr, 0x2000)
raw_ptr = set_remap_bit(raw_ptr)
obj.field = raw_ptr // 就地修正指针,以便下次使用
}
x = raw_ptr // x 现在指向 0x2000
下次任何线程加载 obj.field 时,remap 位已经设置好了。屏障检查通过 fast path,没有额外工作。过时指针在第一次访问时被惰性修正。
这是关键机制。与其像 G1 在疏散期间那样让 GC 停止世界以一次性更新所有指向重定位对象的指针,ZGC 让应用程序在遇到指针时逐个修正。代价是每次指针加载都要支付屏障检查的开销,即便没有任何东西被重定位。在实践中,fast path(检查几位)执行代价足够小,与避免 STW 重定位停顿带来的收益相比,开销很小。
Concurrent relocation (并发重定位)。 G1 停止世界以将存活对象从回收区域中疏散。ZGC 在应用程序运行的同时重定位对象。它能做到这一点是因为加载屏障处理了指针修正。在启动和结束每个阶段(标记开始、标记结束、重定位开始)时有简短的 STW 停顿,但这些通常远低于 1ms。拷贝对象和修正指针的实际工作是并发发生的。
Generational ZGC (Java 21+)。 最初的 ZGC 不按年龄划分堆。分代 ZGC 增加了年轻代和老年代,同时保留了亚毫秒级停顿的保证。它更频繁地回收年轻区域(垃圾最多的地方),较少回收老年代区域。加载屏障和有色指针机制被扩展以处理分代写屏障。
何时使用 ZGC vs G1:

亲自尝试:
# 使用 ZGC 运行
java -Xmx256m \
-XX:+UseZGC \
"-Xlog:gc*:file=gc_zgc.log:time,uptime,level,tags" \
GCDemo
# 使用分代 ZGC (Java 21+)
java -Xmx256m \
-XX:+UseZGC -XX:+ZGenerational \
-Xlog:gc \
GCDemo
观察日志:
[0.318s] GC(0) Garbage Collection (Warmup) 28M(11%)->12M(5%)
[0.321s] GC(0) Pause Mark Start 0.023ms
[0.489s] GC(0) Concurrent Mark 168.123ms
[0.491s] GC(0) Pause Mark End 0.019ms
[0.492s] GC(0) Concurrent Select Relocation Set 1.234ms
[0.502s] GC(0) Concurrent Relocate 10.456ms
STW 停顿是标记为“Pause”的行。其他所有内容都是并发的。将此处的停顿持续时间与 G1 的输出进行对比。
CPython(Python 的参考实现)是“追踪式收集器占主导”模式的主要例外。它使用引用计数作为主要机制,并在之上增加了一层用于追踪循环引用的检测器。
CPython 中的引用计数。
每个 Python 对象都有一个 ob_refcnt 字段。Python 的 C API 在 Py_INCREF 时增加,在 Py_DECREF 时减少。当计数归零时,对象在 _Py_Dealloc 中被立即释放。这赋予了 Python 确定性的销毁:del 方法和上下文管理器的 exit 调用在最后一个引用掉落的那一刻发生。
import sys
x = []
print(sys.getrefcount(x)) # 2: 1个来自x,1个来自getrefcount参数本身的临时引用
y = x
print(sys.getrefcount(x)) # 3: 1个x, 1个y, 1个getrefcount参数
del y
print(sys.getrefcount(x)) # 2: 回到1个x, 1个getrefcount参数
循环引用问题。 仅靠引用计数无法回收循环垃圾。
import gc
# 创建循环引用
class Node:
def __init__(self, name):
self.name = name
self.ref = None
a = Node("A")
b = Node("B")
a.ref = b
b.ref = a # cycle: A -> B -> A
# a 和 b 的计数都 >= 1(由于相互引用)。
# 仅靠引用计数,两者都不会被释放。
del a
del b
# a 和 b 依然存活!Refcount: A 为 1 (来自 b.ref), B 为 1 (来自 a.ref)
# 显式触发循环检测器
collected = gc.collect()
print(f"Collected {collected} objects") # 收集了 4 个对象 (2个node + 2个dict)
引用计数处理了常见情况,但它无法收集循环引用。CPython 的答案是运行在引用计数系统之上的独立循环检测器。其实现在 Modules/gcmodule.c 中。
循环检测器是一个追踪式收集器,但它并不追踪整个堆。它仅跟踪能够参与循环引用的对象:如列表、字典、集合及用户自定义类实例等容器对象。字符串和整数无法持有对其他对象的引用,因此无需跟踪它们。
与 Java 的收集器一样,循环检测器使用分代方法。共有三代,编号为 0、1 和 2。思路与我们之前讨论的分代假说相同:大多数对象死得早,所以经常检查年轻对象,少打扰老对象。默认阈值硬编码在 CPython 的 Modules/gcmodule.c 中:
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{ {(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0},
{ {(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0},
{ {(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0},
};
你可以验证你的运行时实际使用的是什么:
python3 -c "import gc; print(gc.get_threshold())"
# (700, 10, 10)
请注意,某些框架和发行版会在启动时通过 gc.set_threshold() 覆盖这些默认值,因此你的环境可能显示不同的值。
第 0 代持有新分配的容器对象。当自上次回收以来的新分配数量超过阈值(默认 700)时,回收第 0 代。幸存的对象被提升到第 1 代。在第 0 代被回收 10 次后,第 1 代被回收一次。幸存者移至第 2 代。在第 1 代被回收 10 次后,第 2 代被回收一次。
效果是第 0 代大约每 700 次分配回收一次,第 1 代大约每 7,000 次,第 2 代大约每 70,000 次。进入第 2 代的长寿对象几乎永远不会被打扰。检测器将其大部分时间花在最年轻的对象上,这些对象最有可能最近变成了垃圾。
你可以看到这些计数:
import gc
# 当前各代阈值
print(gc.get_threshold()) # (700, 10, 10)
# 当前分配计数: (gen0分配, 自上次gen1回收以来的gen0回收数, 自上次gen2回收以来的gen1回收数)
print(gc.get_count()) # 例如 (342, 8, 2)
# 强制进行全量回收
gc.collect()
# 完全禁用循环检测器 (如果你确定代码中没有循环引用)
gc.disable()
当检测器在某一代码代上运行时,它需要找出哪些对象仅被循环引用保持存活。通过一个例子更容易理解算法。
假设检测器正在查看三个被跟踪的对象:X、Y 和 Z。X 指向 Y 和 Z。Y 指回 X。还有一个局部变量持有对 X 的引用。

步骤 1:拷贝引用计数。X=2, Y=1, Z=1。
步骤 2:减去内部引用。Y 指向 X,所以从 X 的副本中减 1 (X 从 2 变为 1)。X 指向 Y,所以从 Y 的副本中减 1 (Y 从 1 变为 0)。X 指向 Z,所以从 Z 的副本中减 1 (Z 从 1 变为 0)。
步骤 3:检查剩余部分。X 的调整后计数为 1。被跟踪集合之外的某些东西(局部变量)仍然指向它。X 存活。Y 和 Z 虽然调整后计数为 0,但它们可以从 X 到达,因此它们也幸存下来。
现在想象局部变量消失了。X 的引用计数掉到 1 (只有 Y 指向它)。运行相同算法:拷贝 X=1, Y=1, Z=1。减去内部引用:X 变为 0, Y 变为 0, Z 变为 0。每个调整后的计数都是零。被跟踪集合之外没有任何东西指向它们。它们仅因彼此而存在。三者都是垃圾。
这就是核心思想。算法寻找那些存在的唯一理由是同一集合中其他对象的目标。
有一个边缘案例困扰了多年:finalizers(终结器)。
终结器是运行时在对象被销毁前调用的方法,给予其清理外部资源(如文件句柄或网络连接)的机会。在 Python 中,这就是 del 方法。
假设 A 和 B 处于循环中,且两者都有 del 方法。检测器知道它们是垃圾,但要释放它们,它需要打破循环。问题是:哪个 del 先运行?如果你先运行 A 的终结器,而它尝试使用 B,但 B 已经正在被销毁,你就会崩溃。如果你先运行 B 的,而它使用 A,同样的问题。没有安全的顺序。
在 Python 3.4 之前,CPython 直接放弃处理这些对象。它将它们放入名为 gc.garbage 的列表中,且永远不释放它们。如果你的代码创建了带有 del 的循环引用,你就会有一个静默的内存泄漏。PEP 442 通过在打破任何引用之前调用终结器修复了这个问题。当 A 和 B 的 del 运行时,两者都保持完整。只有在所有终结器执行完毕后,检测器才会打破循环并释放对象。
关于 CPython 的内存模型还有一件事值得了解。每当 Python 执行类似 x = some_object 的操作时,它会增加 some_object 的引用计数(C 语言中的 Py_INCREF)。每当变量超出作用域时,它减少计数 (Py_DECREF)。在 C 中这些是普通的整数操作:refcount += 1, refcount -= 1。没有锁,没有原子指令。
在多线程程序中,这是一个问题。两个线程可能同时增加同一个对象的引用计数。如果没有同步,一个增加操作会丢失(经典的竞态条件),之后该对象可能会在有人仍在使用时被释放。
GIL (全局解释器锁) 防止了这种情况。一次只有一个线程执行 Python 字节码,因此两个线程永远不会同时修改同一个引用计数。GIL 免费使所有引用计数操作变得安全,而无需任何原子指令。
这也是移除 GIL 如此困难的原因。如果拿掉它,整个代码库中的每一个 Py_INCREF 和 Py_DECREF 都需要变成原子操作。原子操作比普通整数增量要昂贵得多。Python 3.13 开始附带实验性的 free-threaded 模式,它使用 biased reference counting(偏向引用计数)来降低这种成本:创建对象的线程可以对引用计数进行廉价的非原子更新,只有访问该对象的其他线程才支付原子操作的代价。
每一种现代垃圾回收器都可以映射回 Wilson 在 1992 年描述的两个家族。它们之间的区别在于关于如何最小化停顿、处理并发以及高效管理内存的工程决策。

从这一对比中可以观察到几点:
Wilson 的追踪式家族在服务器运行时占据主导地位。 引用计数用于 Swift、Python 和 Rust 的 Arc,但对于具有高分配速率的托管运行时,追踪式收集器是标准做法。循环引用问题无论如何都需要补充追踪步骤,这增加了复杂性,且无法消除每次修改时的引用计数开销。
分代收集除 Go 以外随处可见。 Java 重度利用了分代假说。Python 的循环检测器使用了三代。Go 最初选择不使用分代收集,因为跨代指针写屏障的开销对 Go 的典型工作负载来说不划算。这种情况可能正在改变:最近的 Go 版本中已经开发了实验性的分代支持。
Compaction (整理) vs No compaction 是一个真正的设计分歧点。 Java 收集器进行整理,这允许 bump-pointer 分配(非常快)并消除碎片。Go 不整理,这意味着它永远不需要更新指向已移动对象的指针(更简单的写屏障,无需读屏障以保证正确性)。Go 通过大小类分配器(size-class allocator)来补偿。这是经典的 Wilson 权衡:拷贝和整理收集器以内存开销和指针更新成本换取分配速度和碎片消除。
ZGC 的有色指针是 Wilson 指针标记 (pointer-tagging) 思想的现代实现。 Wilson 提到过在指针中使用位来存储 GC 元数据。ZGC 将此进一步发展,将标记状态、重映射状态和终结状态直接嵌入 64 位指针。在每次指针加载时检查这些位的加载屏障是 ZGC 为亚毫秒级停顿支付的代价。
基本问题从未改变。 从 roots 开始追踪,标记存活内容,回收其余部分。自 1960 年以来的所有发展都是对 McCarthy 原始洞察的工程改进。
你的“停顿”时刻
GC 的艺术在于平衡。在你的开发生涯中,是否遇到过因为 GC 停顿导致的生产事故?你是倾向于 Go 的极致低延迟,还是 Java G1GC 的高吞吐?
欢迎在评论区分享你的调优经历或吐槽!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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

© 2026, bigwhite. 版权所有.
11月底,腾讯云搞了一波双11返场活动,我买了三年的服务器。
和买了新的电脑或者做了新系统一样,得先把生产环境搞好。
距离Python2.x停止维护大概只有5个月了吧,所以第一要务是升级Python的版本。但是yum是依赖Python2的,所以升级还是会有一些顾虑的。下面是升级的过程:
1 | wget https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz |
1 | yum install make build-essential libssl-dev zlib1g-dev libbz2-dev |
1 | yum install -y make build-essential libssl-dev zlib1g-dev libbz2-dev |
1 | mv /usr/bin/python /usr/bin/python.bak |
将下面文件中的配置修改为Python2.x版本的路径。
1 | /usr/bin/yum |
pip需要依赖setuptools,所以要先安装setuptools。
1 | wget https://files.pythonhosted.org/packages/ce/1d/96320b9784b04943c924a9f1c6fa49124a1542039ce098a5f9a369227bad/setuptools-42.0.1.zip |
https://www.cnblogs.com/zhangym/p/6226435.html
https://www.cnblogs.com/fjping0606/p/9156344.html
https://www.cnblogs.com/fyly/p/11112169.html
这是一个上传图片到github的工具,目前还不是很成熟,不过已经可以实现压缩并上传图片的目的了,对于写博客来说已经够用了。
需要注意的是,因为图床需要一个git目录,同时代码也需要。在使用部分git管理工具时会禁止这种目录的嵌套。因此最好将代码独立运行。可以自己打包,也可以使用我打好的可执行程序。
在这里需要提一句,一般如果只想要下载github上项目的一个目录或者一个文件,可以使用svn进行下载。将文件路径中的文件名/master替换为trunk即可使用下载。
经过配置之后,以后只需要将图片放到todo目录下,执行脚本即可。
编译时需要安装以下运行库
1 | pip install pyyaml |
Ver.0.2 2019-11-17 提交编译的可执行程序;增加配置文件
Ver.0.1 2019-11-12 提交项目

本文永久链接 – https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners
大家好,我是Tony Bai。
在这个技术浪潮汹涌、AI 随时可能掀翻牌桌的时代,每一个程序员心中都悬着一个终极问题:
“我现在的技术栈,还能吃几年饭?”
我们每天都在焦虑地刷着各种技术文章,试图从 Google、Anthropic、OpenAI、Nvidia等的风向中,窥探下一个技术红利期。但这些信息往往零散、矛盾,甚至充满了各种培训机构的“幸存者偏差”。
就在半个多月前,X 平台上的一位技术博主 Mojisola Alegbe,基于 Stack Overflow、GitHub Trends、JetBrains 等多方数据,整理并发布了一份极其残酷的私房版《2026 编程语言“饱和度”榜单》。
这篇推文就像一颗深水炸弹,在短短几天内获得了 41.2 万的惊人阅读量。大批开发者涌入评论区,有人哀嚎,有人庆幸,有人愤怒,有人不屑。这张榜单之所以能引爆全网,因为它赤裸裸地揭示了我们这个行业最真实的“供需关系”和“内卷现状”。
今天,我们就来深度扒开这张榜单背后的血泪与真相。看看你我手中的“锤子”,到底还能敲几年钉子。

让我们先深吸一口气,看看这份令人心跳加速的榜单:
看完这张图,我猜很多人的第一反应是:
但如果事情真的这么简单,那我们这个行业也未免太无趣了。这张榜单真正有价值的地方,在于它炸出了评论区里无数资深架构师和一线开发者的“人间清醒”。
在这张榜单的评论区,你可以看到整个技术圈最真实的生态缩影。
阵营一:饱和焦虑派
“完了,我刚想学编程,这可怎么办?”
“怪不得现在工作这么难找……”
阵营二:不屑一顾派
“语言只是工具,解决问题才是关键。”
“这种指标毫无意义。”
阵营三:人间清醒派(重点看这里!)
这部分评论,往往来自那些穿越了数个技术周期的老炮。他们的观点,破具含金量。
一位开发者一针见血地指出:
“语言的饱和度是个误导性指标。真正的问题不是有多少开发者懂它,而是有多少开发者能用它构建出真正有价值的系统。”
另一位开发者则更加直接:
“饱和度百分比毫无意义。重要的是:你能交付吗(Can you ship)?我只看三个信号:1. 真实的生产环境部署(而不是教程);2. 系统设计的深度(而不只是 CRUD);3. 在压力下调试复杂问题的能力。JavaScript 饱和度 66%?那又怎样,其中 90% 的人连一个可扩展的架构都设计不出来。”
而一位博主,更是给出了顶级玩家的“搞钱思路”:
“聪明的开发者从不追逐‘流行’的语言,他们追逐的是‘高价值’的行业
– Python → AI
– C++ → 高性能系统(游戏、金融)
– Rust → 安全基础设施(区块链、操作系统)
– Go → 云平台(K8s、Docker)
追逐金钱,而不是追逐炒作(Follow the money, not the hype)。”
扒开社区的口水战,我们可以总结出三条极其宝贵的“反内卷”生存法则。
第一条:停止在“语言层”的低水平竞争
如果你是一个 Python 开发者,你的核心竞争力绝对不是“比别人多会几个 itertools 的函数”。
评论区里的一条建议非常中肯:
“不要只学 Python 的语法。去学它底层的 C++ 和 CUDA。这才是 2026 年 AI 热潮中真正值钱的地方。”
同样的道理,如果你是一个前端开发者,让你在面试中脱颖而出的,绝不是多会几个 CSS 动画技巧,而是你对 V8 引擎的内存管理、对大规模前端项目的架构设计、对 WebAssembly 的底层原理的深刻理解。
饱和的永远是“表层应用”,而“底层原理”的护城河,深不见底。
第二条:将你的技术栈,锚定在高价值的“产业赛道”
你选择的语言,决定了你的“工具”;而你选择的行业,决定了你“工具”的价值。
如果你用 Go,但每天只是在写一些简单的 CRUD 业务,那你和用 PHP 的同行并没有本质区别。
但如果你用 Go,去深耕 Kubernetes Operator 开发、去搞 Service Mesh、去做 eBPF 的底层监控,那你将进入一个截然不同的“高价值稀缺区”。
对于大多数开发者来说,最好的策略不是去学一门全新的、不饱和的语言(比如 Zig 或 OCaml),而是在你现有的、最熟悉的语言生态里,找到那个与“高利润、高壁垒”行业结合最紧密的纵深方向,然后一头扎进去。
第三条:从“语言专家”进化为“系统架构师”
评论区里,有一个非常有趣的现象:初级开发者在讨论“哪个语言好”,而资深开发者在讨论“如何交付(Ship)”。
当一个系统变得复杂时,瓶颈往往早已不在于某个语言的语法特性,而在于:
这些“跨语言”的系统设计能力,才是拉开普通程序员和架构师之间收入差距的根本原因。
语言的红利期是短暂的,而架构的复利是终身的。
这张“饱和度”榜单,与其说是一份“死亡通知单”,不如说是一张“体检报告”。它提醒我们,如果你安于现状,只停留在语言的表层舒适区,那么无论你现在用的是 Go 还是 Python,你都随时可能被更便宜、更年轻的开发者所取代。别忘了还有不断“蚕食”初级甚至中高级程序员工作的AI!
在这个充满不确定性的时代,真正的安全感,来源于:
不要再为“哪个语言是宇宙第一”而进行无意义的口水战了。
你的价值,从来不是由你用什么语言决定的,而是由你能用这门语言,解决多大、多复杂、多有价值的问题决定的。
资料链接:https://x.com/yehhmisi/status/2031715243622015239
今日互动探讨:
看完这份榜单,你对自己目前的技术栈感到了焦虑,还是庆幸?在你看来,一个语言的“饱和”是危机,还是意味着更成熟的生态和机会?
欢迎在评论区分享你的看法!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

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

© 2026, bigwhite. 版权所有.

本文永久链接 – https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era
大家好,我是Tony Bai。
如果你回望过去十五年的软件工程史,那无疑是编程语言百花齐放的黄金时代。
为了对抗日益膨胀的系统复杂度,人类绞尽脑汁地发明新的“咒语”:
Google 推出了 Go 语言,用极简的 Goroutine 拯救了深陷并发地狱的后端工程师;
Mozilla 孕育了 Rust,用严苛的所有权机制向内存泄漏和数据竞争宣战;
苹果用 Swift 埋葬了晦涩的 Objective-C;
JetBrains 用 Kotlin 为笨重的 Java的使用者提供了一个更优雅的选择;
微软用 TypeScript 彻底规范了狂野的 JavaScript 生态。
每一次新语言的诞生,都伴随着开发者们的狂欢。我们热衷于讨论语法糖、对比编译速度、争论哪种范式更优雅。我们在各大论坛上为自己喜爱的语言摇旗呐喊。
但这已经是最后的余晖了。
站在 2026 年的节点上,当你看着 Claude Code、Cursor 或各类 Coding Agent 在几秒钟内倾泻出数千行逻辑严密的代码时,一个残酷的真相正在浮出水面:
大模型(LLM)的爆发,彻底抽干了孕育下一代通用编程语言的土壤。属于人类的“造语言”游戏,结束了。
这不是危言耸听,而是基于技术演进第一性原理的必然推演。

在 AI 时代,一门编程语言的生命力不再取决于它的语法有多么优雅,而取决于它在 AI 模型中的“语料权重”。
现存的主流语言(Python, Java, JavaScript, Go, C/C++等)在 GitHub 上积累了数年甚至十余年的海量开源代码。这些代码构成了大模型训练的底座,赋予了 AI 极高的“代码智商”。
当你用 Python 或 Go 提问时,AI 能够瞬间理解你的意图,补全复杂的逻辑,甚至自动发现隐藏的 Bug,因为它的“脑子”里装着上千万个成熟的 Python/Go 示例。
但对于一门新语言来说,这是绝对的死局。
假设明天某个天才发布了一门名为 Nova 的新语言,号称性能超越 C,安全性超越 Rust,语法如 Python 般简洁。
结果会怎样?
这就形成了一个无解的马太效应:
没人写就没有语料 -> 没有语料 AI 就不会写 -> AI 不会写人类就不想学 -> 更没人写。
现存的主流语言通过“语料霸权”,彻底锁死了新语言上升的通道。
人类发明新语言的根本动力,是“人脑的带宽有限”。
C++ 太容易写出内存泄漏,人脑排查太痛苦,所以我们发明了 Rust,让编译器做“真理警察”。
Java 处理异步回调太繁琐(Callback Hell),所以我们发明了各种新的语法糖。
我们一直在努力打造更锋利、更安全的斧头,因为那是人类自己要挥舞的斧头。
但在 Agentic Coding(智能体编程)时代,挥舞斧头的不再是人,而是不知疲倦的 AI。
当你可以用自然语言对 Agent 说:“用 C++ 实现一个高并发的 HTTP 服务器,并严格检查所有内存泄漏风险,写出 100% 覆盖率的测试用例。”
只要 AI 的推理能力足够强,加上自动化的沙箱验证(Eval),它完全可以写出极度安全、高效的 C++ 代码。
如果 AI 能够不知疲倦地处理最繁琐的语法、填补最冗长的样板代码(Boilerplate),并且不出错,那么“语言本身是否易读、是否好写” 似乎就变得不再重要了。
因为代码根本不是给人看的,也不是人写的。当“人脑带宽”不再是瓶颈,发明一种“让人类写得更舒服”的新语言,就失去了最大的现实动机。
如果不再有新的面向人类的通用编程语言,未来的代码世界会变成什么样?
答案是:极端的两极分化。
上层:英语(或自然语言)成为终极编程语言。
Andrej Karpathy 的预言正在成为现实(Software 3.0)。人类不需要学习晦涩的语法,人类只需要学习如何清晰、严谨地表达意图,编写能够精准约束 AI 的 Spec(规格说明书)。我们与机器的接口,退回到了人类最擅长的媒介。
底层:只有机器能读懂的“AI 专属语言”。
如果你是大模型厂商(比如 OpenAI 或 Google),当你发现 90% 的代码都是你的模型生成的,你还会让模型生成冗长、为了兼顾人类可读性而充满妥协的 Java 或 Python 代码吗?
不会的。巨头们极有可能会研发一种专门面向 AI 优化的中间表示语言(Intermediate Representation, IR)。
这种语言对人类来说如同天书,但对于模型来说:
AI 会将人类的自然语言直接“编译”成这种中间码,然后运行。
在这个过程中,介于自然语言和机器码之间、那种专门为了“让人类勉强能懂又能让机器执行”而存在的传统编程语言,其生存空间将被彻底抽空。

这听起来有些感伤,但这就是技术演进的无情车轮。
就像今天,依然有人沉迷于机械表的齿轮咬合,依然有人热爱在暗房里冲洗胶卷。
“纯手工编写代码(Handcrafted Code)”——这种我们曾引以为傲的工业生产方式,未来可能也会退化成一种个人的“艺术爱好”或“思维体操”。我们称之为“古法编程”。
在某个安静的周末,你或许依然会打开编辑器,为了兴趣手撸一段优雅的 Go 并发或者 Rust 生命周期,享受那种久违的、直接控制机器的“心流”多巴胺。
但在残酷的商业战场上,古法编程即将落幕。
不要再为语法糖而争论不休,不要再期待下一个能拯救你的新语言。
去锻炼你的系统思维吧,去学着用自然语言精准地描绘你的蓝图。因为在下一个时代,定义目标的造物主,永远比精通语法的泥瓦匠更稀缺。
你还在坚持“古法编程”吗?
面对 AI 现场生成代码的冲击,你是否还会为了某种语言的“优雅语法”而兴奋?在你的理想中,未来的“AI 专用中间码”应该长什么样?你是更享受亲自掌控每一行代码,还是更向往定义目标的“造物主”角色?
欢迎在评论区留下你对“古法编程”时代的最后致敬!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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

© 2026, bigwhite. 版权所有.
PyAutoGUI 是 GUI 功能强大自动化方案,但 UI 程序的运行环境选择与配置也是一大难题。
为了让环境可迁移,免维护,资源消耗少,必须使用虚拟化的方案。
虽然 PyAutoGUI 支持 Windows/macOS/Linux 三个平台。但是各有各的弊端。
综合以上情况考虑,系统环境选择如下
步骤
从不gnome-screenshot: 用于 PyAutoGUI 内部调用截图其他说明:
虚拟机睡眠唤醒后时间没有更新,不知道为什么没有触发时间更新。
目前只能通过强行同步来解决
在脚本里执行同步命令
1 | # 和阿里云ntp服务器同步 |
或者,理论上可以通过虚拟机中的 systemd-suspend hook,或者 pve hookscript 来解决(没试成功)
pve 默认图片控制台不太方便,不能复制粘贴,所以使用远程登录调试
由于系统使用 x11 环境,这里使用 xrdp 作为远程服务端。
如果以后更新到 wayland,可以使用 gnome-remote-desktop。
1 | sudo apt update |
xrdp 或者 ssh 远程登录执行脚本时,窗口相关命令可能会提示找不到显示器。
可以在自动化脚本中设置以下环境变量
1 | os.environ['DISPLAY'] = ':0' |
gnome-screenshot 截图时会产生一些无用日志
1 | ** Message: 17:36:31.888: Unable to use GNOME Shell's |
新建一个 gnome-screenshot 文件,赋予执行权限,放到 PATH 环境变量中比 /usr/bin 靠前的路径
1 | #!/bin/bash |
常见的方案是 pytesseract, 但是效果不好,识别率比较一般。
安装
1 | sudo apt install tesseract-ocr |
使用
1 | pytesseract.image_to_string(image, config='--psm 8 -c tessedit_char_whitelist=0123456789') |
ddddocr 是基于机器学习的验证码识别库,识别效果比较好。
这里使用 docker 安装 fastapi接口
1 | docker run -d -p 8000:8000 oozzbb/ddddocr-fastapi:latest |
使用
1 | def ocr_image(file='region.png'): |
可以在宿主机远程调用自动化脚本,并且脚本执行完成后挂起虚拟机
1 | HOST=your-name@vm-ip-address |
1 | # 切换窗口到前台 |
PyAutoGUI 是基于图像而不是图型控件来识别目标,没有确认反馈。
相比于网页自动化的 Selenium 就感觉落后些了。
当然,Windows 平台有 pywinauto 支持控件识别,但我不咋熟悉。
总之,PyAutoGUI 就像手枪,虽然比较简陋,但还是很直观的,执行简单的任务也够用了。
我们在测试代码时,由于需要经常重启服务,经常会发现服务端口被占用。
一般kill掉后台进程就ok了,但是如果服务有启动一些常驻的后台程序,可能也会导致端口不能释放。
在类UNIX系统中,一切被打开的文件、端口被抽象为文件描述符(file descriptor)
从python3.4开始,文件描述符默认是non-inheritable,也就是子进程不会共享文件描述符。
一般为了实现多进程、多线程的webserver,服务端口fd必须设置为继承(set_inheritable),这样才能多进程监听一个端口(配合SO_REUSEPORT)
典型的是使用flask的测试服务器的场景,这里我们写一段代码模拟。
1 | import socket, os |
我们通过lsof -p {pid}可以看到这两个进程的所有文件描述符
server进程, 可以看到服务端口的fd是4
1 | COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME |
sleep子进程,也拥有fd=4的文件描述符
1 | COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME |
如果server进程退出时,sleep进程没有退出,fd=4对应的端口就被占用了,服务也就不能正常启动了。
1 | import os |
使用subprocess库而不是os来启动子程序, 通过close_fds参数关闭多余的文件描述符
1 | import subprocess |
循环变量泄露与延迟绑定叠加在一起,会产生一些让人迷惑的结果。
先看看一开始的问题,可以看到这里lambda函数的返回值一直在变。
1 | xx = [] |
输出如下
1 | a: 3 |
由于Python没有块级作用域,所以循环会改变当前作用域变量的值,也就是循环变量泄露。
注意:Python3中列表推导式循环变量不会泄露,Python2中和常规循环一样泄露。
1 | x = -1 |
输出如下
1 | 6 : for x inside loop |
再讲一下闭包,在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。
这里所谓的引用可以也就是内部函数记住了变量的名称(而不是值,这个从ast语法树可以看出),而变量对应的值是会变化的。
如果在循环中定义闭包,引用的变量的值在循环结束才统一确定为最后一次循环时的值,也就是延迟绑定(lazy binding)。
所以下面的例子,xx的所有匿名函数的返回值均为3
1 | xx = [] |
再分析一开始的问题,这里的匿名函数引用了变量i,而i是全局变量,所以再次使用i作为循环变量时,列表中的匿名函数引用的值就被覆盖了。
正确做法:
又是一个python下载地震数据的脚本,不过这次更好玩一些。设置一下就可以定期到USGS官网看看有没有大于某个震级的地震,然后到IRIS下载。

本文永久链接 – https://tonybai.com/2026/03/17/why-is-go-regex-so-slow
大家好,我是Tony Bai。
如果有人问你:在处理纯 CPU 密集型的文本匹配时,Go 和 Python 哪个快?
相信 99% 的 Go 开发者会毫不犹豫地把票投给 Go。毕竟,一门编译型的静态语言,怎么可能输给拖着 GIL 锁的解释型脚本语言?
但现实往往比小说更魔幻。
最近,在 Reddit 的 r/golang 论坛上,一张残酷的 Benchmark 跑分图引发了整个 Go 社区的剧烈震荡。一位开发者,使用极其常见的日志解析正则表达式(提取 IP、时间、URI 等),对各大语言进行了一次横评。

结果令人大跌眼镜:同样的数据集,Rust 跑了 3.9 秒,Zig 跑了 1.3 秒,而 Go 居然跑了整整 38.1 秒!整整比第一名 Zig 慢了接近 30 倍!
如果你再去翻看 Go 官方的 Issue #26623,会看到更绝望的数据:早在2018年的一次正则基准测试中,Go 不仅被 C++ 和 Rust 碾压,甚至连 Python 3、PHP 和 Javascript 都能在正则上把 Go 按在地上摩擦。

一时间,无数 Gopher 信仰崩塌:“为什么 Go 的标准库 regexp 这么慢?”、“连简单的正则都做不好,Go 凭什么做云原生霸主?”
今天,我们就来硬核扒开 Go 语言 regexp 包的底层设计和实现。你会发现,这不是 Go 团队的技术拉跨,而是一场关于“性能、安全与工程哲学”的博弈。

面对“为什么 Go 的正则比 Python 还慢”的灵魂拷问,Go 核心团队成员 Ian Lance Taylor 给出了第一层解释。
在 Python、PHP 甚至 Node.js 中,你以为你是在运行脚本,其实它们底层都在悄悄“作弊”。这些语言的正则表达式引擎,几乎全部是用高度优化的 C 语言库(主要是 PCRE,Perl Compatible Regular Expressions)编写的。
当你在 Python 里调用 re.match() 时,它瞬间就穿透到了 C 语言的底层,享受着现代 CPU 指令集的极致加速。
那 Go 为什么不用 C?因为 Go 是一门有着“极度洁癖”的语言。
如果 Go 的标准库引入了 C 语言的 PCRE,就必须通过 CGO 来调用。而 CGO 的上下文切换成本极高,更致命的是,它会彻底破坏 Go 引以为傲的“跨平台交叉编译”能力。你再也不能在一个简单的 go build 后,把二进制文件无痛丢到任何 Alpine 容器里了。
因此,Go 团队做出了第一个艰难的决定:完全使用纯 Go 语言,从零手写一个正则表达式引擎。
脱离了 C 语言几十年的底层优化积累,用原生代码去硬刚别人的 C 引擎,这是 Go 看起来“慢”的表层原因。
但这,仅仅是冰山一角。
真正让 Go 正则变得“慢”的,是算法架构上的降维选择。这牵扯到 Go 语言的缔造者之一、大神 Russ Cox (rsc) 的一段往事。
在正则表达式的底层世界里,存在着两大流派:
PCRE 引擎极快,它支持各种花里胡哨的语法(如前瞻断言 Lookaround、反向引用 Backreferences)。它的算法逻辑是“不撞南墙不回头”的深度优先搜索(DFS)。在匹配正常字符串时,它快如闪电。
但它有一个极其致命的死穴:ReDoS(正则表达式拒绝服务攻击)。
想象一下你写了一个看似无害的正则:
^([a-zA-Z0-9]+\s?)+$
如果黑客故意传入一个极其恶意的字符串:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!(注意最后的感叹号)。
PCRE 引擎会陷入可怕的“灾难性回溯”。它会尝试所有可能的组合,时间复杂度瞬间飙升到 O(2^n) 级。短短几十个字符的输入,能让单核 CPU 满载运行几年都算不出结果!
2019 年,互联网巨头 Cloudflare 就因为在 WAF 防火墙中写错了一个极其简单的正则表达式,CPU资源瞬间耗尽,导致全球80% 的通过 Cloudflare 代理的网站受到影响,陷入瘫痪长达 27 分钟。这就是 PCRE 回溯引擎的恐怖破坏力。
Russ Cox 在设计 Go 的 regexp 包时,定下了一条铁律:系统安全与可预测性,绝对高于单次请求的极限性能。
因此,Go 彻底抛弃了危险的回溯引擎,选择了基于 Thompson NFA 的算法(源自他之前在Google主导设计的 C++ RE2 引擎)。这种算法保证了匹配时间永远是线性复杂度 O(n)。
无论黑客传入多么恶意的字符串,Go 的正则引擎绝对不会发生灾难性回溯。它牺牲了在美好情况下的极致快感,换取了在极端恶劣环境下的金身不坏。
这算是 Go 团队最顶级的“克制”吧。
既然算法是 O(n) 的,为什么 Go 依然比同样采用 RE2/DFA 思想的 Rust 慢那么多呢?
如果你去追踪 Go 官方的 Issue #19629和Issue #11646,通过 pprof 分析 Go 正则匹配的 CPU 耗时,你会看到几个令人头疼的瓶颈:
1. 沉重的 UTF-8 解析税
Rust 和 C 的很多正则引擎,底层是直接在“字节(Byte)”级别游走的。而 Go 为了贯彻它对 Unicode 的原生支持,regexp 包在内部极其频繁地将输入流解码为 Rune(Go 的 Unicode 字符单位)。这种逐个解析 Rune 的操作,带来了巨大的计算开销。
2. NFA 虚拟线程的内存震荡
在 Go 的底层源码中,你可以看到耗时最高的两个函数是 (machine).add 和 (machine).step。
Go 是通过维护两个“状态队列(稀疏集)”来模拟 NFA 的并行推进的。每读取一个字符,引擎就要把所有可能的状态添加到下一个队列中。这导致了海量的内存重分配(Allocation)和切片拷贝。哪怕是匹配一个简单的长字符串,底层都在疯狂地挪动内存。
既然这么慢,为什么不把 C++ RE2 里那个极速的 DFA(确定性有限状态自动机)移植到 Go 里呢?
Issue #11646 记录了这次尝试。开发者 Michael Matloob 曾经试图将 RE2 的 DFA 移植过来,但被 Russ Cox 拦下了。原因很直接:DFA 虽然快,但它在运行时会动态生成大量的状态,如果不加以严格限制,极易引发内存耗尽(OOM)。在 Go 带有 GC 的内存模型下,频繁创建和销毁庞大的 DFA 状态缓存,会让垃圾回收器不堪重负。
于是,Go 的标准库在“安全、内存、性能”的三角博弈中,选择了妥协于现状。
官方的克制固然令人敬佩,但对于身处一线的业务开发者来说,由于正则太慢导致的 CPU 告警,是实实在在的痛点。
“既然官方不愿意改,那我们就自己造轮子!”
在近期的 Issue #26623 中,一位名为 kolkov 的开发者带着他的开源库 coregex 杀入了战场,向 Go 标准库发起了直接的挑战。
coregex 是一个完全用纯 Go 编写的正则库,它的出现直接将 Go 的正则性能拉到了与 Rust 并驾齐驱,甚至在某些场景下超越 Rust 的境地。
它是怎么做到的?它在底层祭出了几个大杀器:
在 kolkov 提供的 CI 跑分中,在 6MB 的输入下,coregex 处理邮箱、URI 的耗时仅为 1.5 毫秒,而标准库耗时高达 260 毫秒。足足快了 170 倍!
然而,这段极其硬核的改进,依然很难入Go团队法眼,更不用谈在短期内被合并进 Go 的标准库。
一方面,Go 官方目前正在推进自己的内建 SIMD 方案(Issue #73787),不想接入手写的汇编代码;另一方面,社区大牛 Ben Hoyt 在使用 coregex 时发现,如果开启 Longest() 模式(最长匹配模式),这个库的性能会发生严重退化。
这再次印证了标准库开发的残酷:在某几个特定场景下跑到全宇宙第一很容易,但要在一套 API 里无死角地兜底全世界所有的奇葩正则输入,难如登天。
大致了解了底层原理,回到日常开发中,我们该如何应对 Go 正则的性能瓶颈?作为高级 Go 开发者,请务必将以下三条军规刻在脑子里:
第一条:能不用正则,就坚决不用
如果你只是想检查字符串是否包含子串,或者进行简单的前后缀匹配,永远优先使用 strings.Contains()、strings.HasPrefix() 等内置函数。 它们底层有优化的实现,在这样简单场景下,速度是 regexp 包不可比拟的。
第二条:将编译前置,远离循环
如果你翻看新手代码,最常见的低级错误就是在 for 循环或者每次 HTTP 请求里调用 regexp.Compile()。
正则的编译过程(生成 NFA 字节码)极其消耗 CPU。请永远在全局变量或 init() 函数中使用 regexp.MustCompile(),将其编译好并复用。Go 的 Regexp 对象是并发安全的,随便多 Goroutine 调用。
第三条:在极端性能要求下,打破“洁癖”
如果你的核心业务(比如高频日志清洗、海量数据 ETL)确实被 regexp 卡住了脖子,不要硬抗。
你可以选择引入通过 CGO 调用 PCRE的Go binding库(比如https://github.com/GRbit/go-pcre),但要注意防范 ReDoS 攻击,或google/re2的Go binding(比如https://github.com/wasilibs/go-re2),又或是在业务侧尝试社区的野路子 coregex。在生存面前,架构的“洁癖”是可以适当妥协的。
“为什么 Go 的正则这么慢?”
这并非一个简单的工程失误。它是一道分水岭,隔开了“追求跑分好看的玩具代码”与“守护千万级并发集群的生产级设计”。
Russ Cox 宁愿忍受整个开源界的群嘲,也没有为了刷榜而去引入危险的回溯引擎。这或许就是 Go 语言能够成为云原生时代头部语言的原因:不盲目追求上限的巅峰,而是死死守住安全下限。
今日互动探讨:
在你的日常开发中,有没有被由于“写了糟糕的正则表达式”而导致 CPU 飙升 100% 的惨痛经历?你又是如何排查和优化的?
欢迎在评论区分享你的血泪史
认知跃迁:读懂底层机制,才能看透系统架构的本质
从放弃 CGO 选择纯 Go 实现,到防范 ReDoS 采用 NFA,再到社区为了榨干 CPU 性能而引入 SIMD。Go 语言的每一个看似“不合理”的设计背后,都隐藏着深邃的系统级考量。
然而,令人遗憾的是,很多开发者写了五六年的 Go 代码,遇到性能瓶颈依然只能靠“瞎猜”和“重启”。他们对 Go 的内存逃逸、Goroutine 调度机制以及标准库的底层数据结构一无所知。
如果你渴望突破“熟练调包侠”的瓶颈,想要像 Russ Cox 这样的顶级大厂架构师一样,看透 Go 语言背后的底层逻辑,建立起自己坚不可摧的技术护城河——
我的极客时间专栏 《Tony Bai·Go语言进阶课》 正是为你量身定制。
在这 30+ 讲极其硬核的内容中,我不仅带你剥开语法糖,深挖 Goroutine 调度、Channel 哲学;更会带你全面吃透 Go 的工程化实践,把底层性能调优背后的逻辑一次性讲透。
目标只有一个:助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变!
扫描下方二维码,加入专栏。不要用战术上的勤奋,掩盖战略上的懒惰。让我们一起用架构师的视角,重新认识 Go 语言。

还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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

© 2026, bigwhite. 版权所有.
部分开源项目源码。
专业的服务器监控和管理工具,提供实时系统监控、性能测试、数据库检测等功能。本项目包含两个版本:独立PHP探针和WordPress插件版本。
php8-probe/
├── phpprobe.php # 独立PHP探针(可直接访问)
├── php-probe-widget/ # WordPress插件版本
│ ├── php-probe-widget.php # 主插件文件
│ ├── includes/ # 小组件类
│ ├── css/ # 前端样式
│ ├── js/ # 前端脚本
│ └── README.md # 插件详细文档
├── LICENSE # 许可证
└── README.md # 本文件
快速开始phpprobe.php 上传到您的Web服务器特点:
无需安装,直接使用
单文件部署,简单方便
支持多平台(Linux、Windows、macOS、FreeBSD)
实时系统监控php-probe-widget 文件夹复制到 wp-content/plugins/ 目录https://gitee.com/obaby/php8-probe
基于Flask和jieba的本地HTTP分词服务。
https://gitee.com/obaby/baby-jb-server
这是一个用于分析 WordPress 博客数据的 Python 工具,可以通过 WordPress REST API 获取并分析博客的文章和评论数据。
统计指定年份发布的文章数量(按月统计)
统计指定年份的评论数量
分析评论用户的评论数排行
将分析结果保存为 JSON 文件https://gitee.com/obaby/baby-wp-data-analysis-tool
一个用于 macOS 系统的微信双开自动化脚本,通过复制微信应用并修改 Bundle ID 实现真正的微信双开功能。
功能特性
一键双开 – 自动完成所有设置步骤
智能检测 – 自动检测已存在的 WeChat2.app
安全可靠 – 完善的错误处理和权限检查
彩色输出 – 友好的命令行界面
进程管理 – 查看和管理微信进程
自动化设置 – 无需手动执行复杂命令https://github.com/obaby/baby-wechat
基于百度地图的足迹地图。
启动服务之后,先去后台 地图 key 设置页面,添加百度地图浏览器端 ak!
启动服务之后,先去后台 地图 key 设置页面,添加百度地图浏览器端 ak!
启动服务之后,先去后台 地图 key 设置页面,添加百度地图浏览器端 ak!
为了防止 js 地址解析受限,需要同时添加服务端 ak!
为了防止 js 地址解析受限,需要同时添加服务端 ak!
为了防止 js 地址解析受限,需要同时添加服务端 ak!
添加之后,访问: http://127.0.0.1:10099/api/location/process-my-location/ 地址刷新数据库的地点坐标信息,后续无需再通过 js 接口进行解析!
https://github.com/obaby/BabyFootprintV2
Add a microblog to your site; display the microposts in a widget or using a shortcode. 增强版优化页面显示,增加分页功能。wp微博插件。
https://github.com/obaby/Simple-microblogging-wordpress-plugin
一个强大的WordPress评论过滤插件,支持字数限制、中文检测、关键词过滤等功能。
评论过滤功能
管理功能
技术特性
https://github.com/obaby/baby-wp-comment-filter
WinRAR is a trialware file archiver utility for Windows, developed by Eugene Roshal of win.rar GmbH.
It can create and view archives in RAR or ZIP file formats and unpack numerous archive file formats.
WinRAR is not a free software. If you want to use it, you should pay to RARLAB and then you will get a license file named "rarreg.key".
This repository will tell you how WinRAR license file "rarreg.key" is generated.
WinRAR uses a signature algorithm, which is a variant of Chinese SM2 digital signature algorithm, to process the user’s name and the license type he/she got. Save the result to “rarreg.key” and add some header info, then a license file is generated.
https://github.com/obaby/winrar-keygen
一个功能强大的WordPress设备管理系统插件,支持设备分组管理、设备信息管理、自定义排序、状态跟踪等功能。
https://github.com/obaby/Baby-Device-Manager
为 WordPress RSS Feed 提供美观的网页展示样式(基于 RSS.Beauty 的 Pink 主题)。
application/xml,使浏览器按 XML 解析并应用 xml-stylesheet。pink.xsl。需在 OpenResty/Nginx 中为 .xsl 配置正确的 Content-Type(见下方配置说明),否则浏览器可能不按 XSL 解析。https://cnb.cool/oba.by/rss-beauty
Contributors: obaby
Donate Link: https://oba.by
Tags: useragent, user-agent, user agent, web, browser, web browser, operating system, platform, os, mac, apple, windows, win, linux, phone
Requires at least: 2.0
Tested up to: 6.3
Stable tag: 16.06.99
插件支持四种 IP 查询方式,可在 设置 → WP-UserAgent 中选择:
| 方式 | 说明 |
|---|---|
| IP2Location | 使用 IP2Location 数据库(需将 BIN 文件放入 show-useragent/ip2location_db/db/),依赖 Composer |
| CZDB | 使用纯真 CZDB 数据库(需授权与 db 文件放入 show-useragent/czdb/db/),依赖 Composer |
| ip2region | 使用 ip2region xdb(仅内置 ip2reginapi,不依赖 Composer)。需将 xdb 文件放入 show-useragent/ip2region_db/,文件名:ip2region_v4.xdb、ip2region_v6.xdb |
| 纯真QQWRY | 使用 qqwry_api(qqwry.dat + ipv6wry.db),无需 Composer。数据文件放入 show-useragent/qqwry_api/ipdata/ |
选择 ip2region 或 纯真QQWRY 时不会加载 vendor/autoload.php。若选择 IP2Location 或 CZDB 时 vendor 加载失败,插件会自动回退为 ip2region 模式,避免站点白屏。
WP-UserAgent is a simple plugin that allows you to display details about a computer’s operating system or web browser that your visitors comment from.
It uses the comment->agent property to access the User-Agent string. Through a series of regular expressions, this plugin is able to detect the operating system and browser which can be integrated in comments or placed in custom places through your template(s).
I’m adding new web browsers and operating systems frequently, as well as updating and optimizing the source code. Your feedback is very important, new features have been added by request, so if there’s something you would like to see in WP-UserAgent, leave a comment, and I’ll see what I can do.
WP-UserAgent was written with Geany – http://www.geany.org/
Images created with The Gimp – http://www.gimp.org/
注意:
- 使用 CZDB 时:若更新替换纯真数据库,请同步更新
show-useragent/ip2c-text.php中的$key = 'n2pf2+PrE1y9I55MjdpLpg==';- 使用 ip2region 时:将 xdb 文件放入
show-useragent/ip2region_db/(ip2region_v4.xdb、ip2region_v6.xdb),无需 Composer。
https://cnb.cool/oba.by/wp-useragent

本文永久链接 – https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance
大家好,我是Tony Bai。
随着 Claude Code、Gemini Cli、Codex 等 AI 编程工具的全面普及,“让 AI 写代码”已经从极客的玩具变成了日常的生产力。随之而来的是一个触及灵魂的问题:哪种编程语言最适合交给 AI 去写?
作为 Gopher,我们一直为 Go 语言的“极简语法”、“极速编译”和“强类型安全”感到自豪。我们理所当然地认为,这种没有任何隐式魔法、像白开水一样的语言,绝对是 LLM 的最爱。
然而,现实总是比直觉更骨感。近日,Ruby 核心提交者 Yusuke Endoh(@mame)发布了一份名为 ai-coding-lang-bench 的硬核定量测评报告。他使用 Claude Code(Opus 4.6 模型)对 13 种主流编程语言进行了系统性横向对比。
在这场涵盖了动态语言、静态语言和函数式语言的混战中,Go 语言的表现究竟如何? 是力压群雄,还是黯然失色?那些备受人类推崇的静态类型系统,在 AI 面前是否成了累赘?
本文和大家一起阅读和拆解这份报告,为你揭晓 AI 时代的语言偏好图谱。

在评价这份报告之前,我们先来看看它的实验设计,这是目前业内少见的、针对 AI Agent 的工程化能力的量化评估。
任务目标:让 Claude Code (Opus 4.6) 从零开始实现一个 mini-git(简化版的 Git)。这是一个极具代表性的任务,它包含了文件 I/O、哈希计算、数据结构操作以及命令行接口,足以考验模型对语言生态的综合运用能力。
测试被巧妙地分为两个阶段,模拟了真实的软件生命周期:
提示词(Prompt)极其极简:“阅读 SPEC-v1.txt,实现它,并确保 test-v1.sh 测试通过。”这种设计最大程度地减少了人类指令的干预,纯粹考验 AI 代理在闭环环境下的自主编码、调试和测试能力。
参赛选手(13种语言/15种配置):
每种语言配置运行 20 次,以消除 LLM 生成的随机性带来的误差,并统计其耗时(Time)、成本(Cost,即 Token 消耗)和代码行数(LOC)。
如果仅看总耗时和总成本(v1 + v2),测试结果呈现出了令人瞩目的阶梯式分布。

在这场 AI 编程竞速中,Ruby(73.1s)、Python(74.6s)和 JavaScript(81.1s)组成了无可争议的第一阵营。
它们不仅生成速度最快、消耗 API 成本最低(均在 $0.40 以下),而且在 20 次测试中表现出了极高的稳定性(标准差极小)。
对于 AI 来说,生成这三种语言的代码就像呼吸一样自然。它们无需繁琐的项目初始化配置(如 Cargo.toml 或 package.json),可以做到“建个文件直接跑”,这种极简的工作流在 v1 阶段(新项目构建)优势极大。
现在,来回答大家最关心的问题:Go 表现如何?
答案是:位居第二梯队。Go 的总耗时为 101.6s,平均成本 $0.50。中规中矩。Go 虽然在语法上非常克制,但依然落后于 Python 和 JS 等动态语言。与之类似,Java(115.4s)也因为繁琐的语法结构和强类型约束,留在了这一梯队。
尽管如此,Go 在整个 20 次测试中没有出现一次失败(0 次 fail),这证明了 Go 的编译器在防止 AI 产生“幻觉 Bug”方面,发挥了极其可靠的安全网作用。
备受人类极客推崇的 Rust(113.7s,且有 2 次失败)和底层的 C(155.8s)在测试中显得步履维艰。
尤为值得注意的是,在总共 600 次的独立运行中,只有 Rust (2次) 和 Haskell (1次) 出现了测试失败(未能最终跑通 Shell 脚本)的情况。这两门语言都以其极高的心智负担和“编译器教你做人”的严格程度而闻名。
这也是将Rust列入“后进生”阵营的主要原因。如果用《飞驰人生》的拉力赛来比喻,Rust 相当于在40站的赛季中,有两站没能完赛!
在传统的工程视角中,“静态类型防止低级 Bug”、“动态语言难以维护”是金科玉律。但在 LLM 驱动的 Agent 开发流中,这个逻辑为何失效了?作者 Yusuke Endoh 提出了几个关键的解释维度。
LLM 的能力直接取决于训练语料的规模和质量。Python、JavaScript 和 Ruby 是过去十几年 Web 开发的绝对主流。GitHub 上海量的这三种语言的开源代码、StackOverflow 上的问答,为 Claude Code 提供了极其丰富的“预训练肌肉记忆”。
当 AI 需要实现一个功能时,它在 Python 的隐空间(Latent Space)中寻找最优解的路径,远比在 Haskell 甚至 Rust 中要清晰、笔直得多。
静态类型系统的初衷是约束人类,防止我们在重构时犯错。但在 AI 的“ Prompt -> 生成 -> 测试报错 -> 思考 -> 再生成”的迭代循环中,严格的类型检查反而成了巨大的“摩擦力”。
报告中一个非常有意思的对照组是:原生动态语言 vs 附加类型检查器的动态语言。
这证明了,迫使 AI 在动态语言中编写严谨的类型注解(Type Annotations),是一项极其昂贵的任务。模型需要耗费额外的算力去推导类型、生成类型声明文件,并且在类型检查器报错时,还要去修复那些在纯动态模式下可能根本不影响运行的“伪 Bug”。
我们通常认为,写得越少,跑得越快。但数据打破了这个迷思。
Haskell 和 OCaml 生成的最终代码行数(224行和 216行)是所有语言中最少的,甚至少于 Python 和 Ruby。然而,它们在生成时间上的表现却排在倒数(Haskell 耗时最长,达 174s)。
这表明,对于 AI 来说,函数式语言那种高度抽象、信息密度极大的代码,生成和推理的成本远高于像 Python、Go 那种稍微啰嗦但逻辑平铺直叙的“大白话”代码。浓缩的未必是精华,对于 LLM 来说,高度浓缩往往意味着更高的生成熵和更高的试错概率。
面对这份详实的基准测试报告,无论你是 CTO 还是普通开发者,都必须开始重新审视未来的技术选型逻辑。
如果你正在启动一个新项目,或者需要用 AI Agent 快速验证一个业务流程,Python 和 TypeScript 是首选(报告中 JavaScript 表现优于 TS,但在实际工程中 TS 的综合权衡更佳)。
不要迷信“大型项目必须一开始就上强类型编译语言”。在需求快速变化的初期,让 AI 用动态语言狂飙突进,是获取业务反馈最高效的手段。
看到测评数据,很多 Gopher 可能会感到失落:难道注重工程严谨性和系统级性能的静态语言,真的在 AI 时代掉队了吗?
结论并非如此悲观。我们需要明确一点:Agent 测评的速度,不等于软件最终运行的速度。
我们不应期待 AI Agent 能够像写 Python 脚本一样,如德芙般丝滑地生成出一个复杂的 Go 并发系统。但在 AI 给出的初稿之上,Go 语言极佳的可读性和统一的规范,将为人类工程师的最终审查(Code Review)节省巨大的精力。
ai-coding-lang-bench 给我们上了生动的一课。它揭示了当前 LLM 的偏好:它们喜欢有海量训练数据的、灵活的、不需要应对死板编译器的语言。
但我们必须认识到,这只是一份基于 2026 年初模型(Claude Opus 4.6)的快照。未来的 AI 编程语言形态,可能会朝着两个方向演进:
无论如何,浪潮已经来临。在 AI 主导代码生成的新时代,我们评价一门编程语言的标准,正在从“它对人类大脑是否友好”,悄然转变为“它对大模型推理是否友好”。
而在这场新赛道上,动态语言们,已经抢跑了。
本文核心数据与图表均来源于 GitHub 项目 mame/ai-coding-lang-bench。
你的 AI 编程初体验
看完这个排名,你是感到意外,还是早已感同身受?在你日常使用 AI 编程时,你觉得它写哪种语言最让你省心?你是否也曾为了修一个 AI 写的编译错误而陷入“死循环”?
欢迎在评论区分享你的“AI 协作”红黑榜!
“语言的严格性正在变成 AI 的摩擦力?在 AI 时代,掌握一套能驱动 Agent 自动化、自修复的‘工作流’比死磕语法更重要。我的新专栏 《AI原生开发工作流实战》 将教你如何利用 Claude Code 结合 Spec 驱动开发,构建真正高产出的‘软件工厂’。”
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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

© 2026, bigwhite. 版权所有.

本文永久链接 – https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents
大家好,我是Tony Bai。
当我们在谈论 AI 编程时,Python 似乎是那个无需讨论的“默认选项”。
然而,随着 AI 应用从模型训练(Training)走向自主智能体(Agents)和复杂的工程落地,基础设施层的语言选型正在悄然发生变化。近日,开源数据编排工具 Bruin 的作者发表了一篇题为《Go 是开发 AI Agents 的最佳语言》的文章,在 Hacker News 上引发了数百条跨语言阵营的激烈辩论。
为什么一位有着 10 年 Python 和 JS 经验的开发者,最终选择用 Go 来构建现代 AI 基础设施?在 AI 生成代码(AI-Generated Code)日益普及的今天,编程语言的“静态类型”、“编译速度”和“语法极简主义”又被赋予了怎样的新维度价值?
本文将深度拆解这场争论,带你探讨在“Vibe Coding(氛围编程)”时代,Go 语言如何凭借其独特的设计哲学,意外地命中 AI Agent 开发的甜点。

Bruin 是一个开源的 ETL(提取、转换、加载)工具。在数据工程领域,Python 拥有统治级的地位(Pandas, Airflow 等),按理说,Bruin 完全应该用 Python 编写。
但作者最终选择了 Go。原因在于,AI Agent 和数据编排工具在本质上属于基础设施(Infrastructure),它们面临的工程约束与模型训练截然不同:
除了上述硬核的工程指标外,作者还坦诚地分享了一个极其主观,但对初创团队至关重要的考量:开发体验(Developer Experience)与情绪价值。
作者将在很长一段时间内作为项目的核心贡献者,他深刻地意识到:
“对于一个小型团队来说,在构建大型项目时,快乐和活力(Joy and Energy)是最稀缺的资源之一。因此,至关重要的是,我不能对自己每天要面对的技术栈感到畏惧或厌烦。”
Go 语言或许在某些特性上不如 Python 灵活,也不如 Rust 表达力强,但它带来的那种“一切尽在掌握”的确定性和快速获得反馈的成就感,能让开发者在漫长的马拉松式开发中保持心流状态。这种心理层面的正向反馈,在 AI Agent 这种充满不确定性的前沿领域探索中,往往是支撑团队走过低谷、坚持到黎明的关键力量。
如果说以上只是 Go 作为“云原生王者”的常规操作,那么在引入大语言模型(LLM)作为“代码生成器”后,Go 的语言特性产生了奇妙的化学反应。
当 Coding Agent 开始每分钟吐出成千上万行代码时,最大的挑战不再是“如何生成”,而是“如何证明它有效”。
在解释型语言(如 Python 或 JavaScript)中,代码的正确性往往只有在运行到特定分支时才能被验证。作者指出,这是 Go 在对抗 AI 幻觉时最大的优势之一:Go 是一门强类型的编译型语言。
当你用 LLM 生成 Go 代码时,go build 成了一道天然且严苛的防火墙。类型不匹配、未使用的变量、错误的函数签名——这些占据了 AI 幻觉相当大比例的低级错误,会被 Go 编译器瞬间无情地驳回。
正如一位 HN 网友 所言:
“在这个人人都在‘氛围编程(vibing left and right)’的时代,你迫切需要一个编译器在背后支持你。Go 让你可以写稍微随意一点的代码,但又不会像 Python 或 JS 那样毫无底线。编译器扮演了看门人的角色,将混乱控制在一定范围内。”
讲到编译期安全,Rust 绝对是无可争议的王者。但为什么作者认为 Go 比 Rust 更适合 AI Agent?
简单来说:Rust 的上限极高,但门槛太陡;Go 用 20% 的努力(快速编译+GC),换取了 80% 媲美 Rust 的安全性,这恰好是 AI 迭代的最优解。
Go 语言自诞生起,就因为其语法的“无聊”和“死板”(比如缺乏灵活的宏、长期没有泛型、繁琐的错误处理)而饱受争议。然而,在 AI 时代,这种“无聊”却意外地成为了巨大的优势。
Python 和 JavaScript 以“灵活”著称。在一个 JS 项目中,有人用 CommonJS,有人用 ES6 Modules;有人用 npm,有人用 pnpm。对于人类来说,这叫“生态繁荣”;但对于 LLM 来说,这叫“状态空间爆炸”(High Entropy)。
Go 是极其“固执”的语言(Opinionated)。
正如作者指出的:“要求 Agent 格式化 JS 代码,它会去引入一个新工具并尝试配置它;而在 Go 中,它只需要运行 gofmt。”
这种高度统一的代码风格,意味着在 LLM 的训练语料库中,Go 代码的“信噪比”极高。模型不需要在多种编程范式中猜测你的偏好,它输出的 Go 代码通常具有高度的同质性和可预测性。
当 AI 成为主要的“代码编写者”时,人类的角色将不可避免地向“代码审查者(Code Reviewer)”倾斜。
如果 AI 生成了一段高度抽象的 Haskell 代码,或者使用了大量宏的 Rust 代码,人类审查者需要耗费极大的脑力去反编译这些逻辑。
而 Go 代码是出了名的“所见即所得”。没有隐藏的控制流,没有复杂的运算符重载。当 AI 生成了几百行 Go 代码时,即使是一位初级开发者,也能相对轻松地顺着逻辑线读懂它在干什么。
在 AI 编程的下半场,“代码易读”将比“代码易写”重要一万倍。
当然,这篇文章在 Hacker News 上并非一边倒的赞同。不同语言阵营的开发者提出了极其犀利的反思。
Python 拥护者指出,文章混淆了“运行时性能”和“开发生态”。
虽然 Go 在高并发和 I/O 上碾压 Python,但如果 AI Agent 的核心逻辑涉及大量的数据科学计算、复杂的概率模型,或者需要直接调用底层的 C++ 机器学习库,Python 依然是不可替代的粘合剂。对于许多初创团队来说,“让代码先跑起来”远比“让代码跑得快”更重要。
支持函数式语言(如 OCaml, F#)的开发者指出,Go 的类型系统依然过于薄弱。
Go 缺乏代数数据类型(ADT)和模式匹配,导致其虽然能抓住低级语法错误,但难以像 Rust 或 OCaml 那样“在编译期保证业务逻辑状态的正确性”。
对于他们而言,如果 AI 真的足够聪明,应该让 AI 生成具有极强类型约束的代码,把正确性完全交给编译器,而不是像 Go 那样依然需要编写大量的单元测试。
这是一个终极的哲学问题:如果未来 AI 不再犯错,能够零成本生成正确的机器码,高级编程语言还有存在的意义吗?
有评论认为,当模型能力足够强时,我们甚至不需要编译型语言的保护,直接用自然语言(英语)+ LLM 生成运行时的 WebAssembly 可能才是终局。在这个维度上,争论 Go 还是 Python,就像在争论用什么牌子的算盘(意指已经被时代所抛弃的东西)一样没有意义。

在 AI 技术日新月异的当下,我们往往容易陷入一种对“前沿”的盲目崇拜,认为只有最复杂的语言、最先进的模型才能构建出优秀的系统。
但 Bruin 作者的实践和 Go 社区的繁荣告诉我们另一个故事:工程的本质是权衡(Trade-off)。
Go 并不是世界上最完美的语言,它的类型系统不如 Rust 严谨,它的生态不如 Python 庞大。但它用极致的编译速度、简单的并发模型、出色的内存管理和统一的编码规范,构建了一个容错率极高的工程基座。并且在这个基座上,无论是人类还是 AI Agent,都能以最低的“认知摩擦力”输出可靠的工业级代码。
资料链接:
你更相信谁?
在 AI 编程的下半场,语言的地位正在重构。你是坚守 Python 的生态优势,还是更看好 Go 在“基础设施级 Agent”中的爆发?你认同“编译器是 AI 的最佳守门员”这个观点吗?
欢迎在评论区留下你的“阵营宣言”!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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

© 2026, bigwhite. 版权所有.

本文永久链接 – https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory
大家好,我是Tony Bai。
在系统级编程语言的版图上,Go 与 Rust 的对比与争论从未停歇。一个是崇尚大道至简、开发效率极高的“云原生时代王者”;另一个则是以内存安全、零成本抽象和极致性能著称的“极客新宠”。当这两种哲学碰撞在对安全性、稳定性和低延迟要求极高的金融/交易基础设施领域时,开发者该如何抉择?
近日,在 Reddit 的 r/golang 社区中,一场由 Python 开发者发起的关于“金融基础设施长期演进:Go 还是 Rust?”的技术讨论引发了广泛关注。这位开发者试图为机器学习(ML)流水线、分布式后端和内部 DevOps 工具选择一门强类型语言,并一度陷入了“是否应该同时学习两者”的焦虑中。
这场社区讨论不仅揭示了两种语言在现代架构中的真实定位,更展现了 Go 社区一贯的“务实主义”工程哲学。本文将深度提炼这场讨论的核心观点,为正处于技术选型十字路口的架构师和开发者提供极具价值的参考。

在金融科技(FinTech)和交易系统中,有两个指标至关重要:性能(Performance/Latency)与 正确性(Correctness)。这恰好对应了系统级语言常常被审视的两个维度。
许多开发者最初被 Rust 吸引,正是因为其在金融领域展现出的“绝对严谨”。
然而,正如资深工程师在讨论中指出的:“Rust 的高壁垒不仅体现在初始学习成本上,更体现在它持续要求你的大脑处于高速运转状态。” 在编写普通业务代码时,开发者需要不断与编译器“搏斗”,这在无形中拖慢了业务交付(Shipping)的速度。
面对 Rust 强大的理论优势,Go 社区给出的回应并不是在极限性能上去硬碰硬,而是打出了一张工程学上的王牌:投入产出比(ROI)。
讨论中一个非常核心的洞见是:不要试图用一种语言解决所有问题,而是要看清具体领域的边界。楼主的背景是 Python,主要涉及 ML 流水线。这引出了现代架构中非常经典的一种组合模式。
这种架构下,系统被清晰地划分为:Go 负责将数据又快又稳地搬运和路由,Python(在底层 C/C++ 的加持下)负责纯粹的数学和模型计算。这种解耦使得整个系统既享受了 Python 的生态红利,又获得了 Go 在分布式系统上的强悍工程能力。
不可忽视的是,当讨论深入到金融领域的最底端——高频交易(HFT)时,社区展现出了极度客观的技术视野。
多位业内人士指出,在纳秒必争的超低延迟交易领域,C++ 依然是绝对的霸主。尽管 Rust 在试图切入这一市场,但 C++ 在传统金融领域积累的庞大库、成熟的生态以及直接操作硬件的能力,短期内难以被撼动。因此,如果业务的核心真的是 HFT,那么 Go 和 Rust 可能都不是最优解。这就进一步确认了 Go 的主战场:高吞吐的分布式后端与云原生基础设施。
在架构决策中,语言的特性往往只占 50%,另外 50% 则是关于人的管理。这也是本次社区讨论中,Go 获得压倒性支持的关键原因。
“在商业应用中,我更看重随着时间的推移,修改代码有多难。业务需求在不断变化,代码也必须随之改变。”
“如果你用 Rust 构建了一个工具,当系统在半夜发生故障时,团队里的其他人能轻易地看懂代码并修复它吗?”
Go 的创造者之一 Rob Pike 曾明确表示,Go 的设计初衷就是为了解决 Google 内部大型团队的协作问题。Go 的语法少、规范统一(gofmt),被称为“没有魔法的语言”。一个有其他语言基础的程序员,通常只需一两周就能熟练上手 Go 并提交生产代码。
相比之下,熟练的 Rust 开发者在市场上不仅稀缺,而且薪资高昂。对于一家非底层技术驱动的金融公司而言,使用 Go 可以极大地降低招聘门槛和团队代码交接的风险。

回到这位发帖者的终极问题:“我应该同时深入学习 Go 和 Rust 吗?”
社区给出的答案异常一致:绝对不要。 尤其是在项目初期。同时学习两门底层逻辑截然不同的语言,不仅会带来巨大的认知撕裂,还会严重拖慢项目进度(Shipping speed)。
最终,这位发帖者更新了他的决定:选择 Go。
“我不想在开始阶段就陷入困境,既然我是独立开发,我开始觉得 Go 才是正道。对于沉重的数学计算,我会继续让 Python 负责。我意识到 Go 真的非常好用,只要我懂得正确使用它,它能在所有的用例中大显身手。此外,Go 社区是我见过最友好的社区之一,你们太棒了!”
在 AI、区块链、量化金融等技术泡沫层出不穷的今天,技术选型很容易陷入“追逐时髦”(Hype Driven Development)的陷阱。Rust 无疑是一门伟大的语言,代表了系统编程的未来探索。然而,Go 语言的伟大之处在于它始终保持着极其清醒的工程边界感。
它不追求类型理论的极致完美,也不苛求消除最后百分之一的性能损耗,它追求的是:在开发者心智负担、编译速度、运行性能、并发模型和部署便利性之间,找到一个无可挑剔的全局最优解。
对于现代分布式系统、网络服务和金融后端基础设施而言,Go 依然是那个能够让你“早点下班、安心睡觉”的最优选择。这也是务实主义在工程世界里,又一次漂亮的胜利。
资料链接:https://www.reddit.com/r/golang/comments/1ra0dza/go_vs_rust_for_longterm_systemsfinance/
你怎么选?
软件工程永远是权衡的艺术。在你看来,对于非高频交易的后端业务,Rust 带来的安全性是否足以抵消它的开发成本?如果你现在接手一个新项目,你会优先选择“能让你早点下班”的 Go 吗?
欢迎在评论区分享你的选型“心法”!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

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

© 2026, bigwhite. 版权所有.

本文永久链接 – https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways
大家好,我是Tony Bai。
在 2026 年的今天,人工智能早已走出了实验室,成为企业级应用的核心驱动力。Python,凭借其在机器学习领域的绝对统治地位——拥有 PyTorch、TensorFlow、Hugging Face 等无可匹敌的生态系统——长期以来被视为 AI 开发的“默认语言”。
然而,随着 AI 应用从模型训练(Training)走向推理服务(Inference)和应用编排(Orchestration),工程重心发生了微妙的转移。当我们谈论模型本身时,Python 是王者;但当我们谈论承载模型流量的基础设施——网关、代理、路由器时,Python 还是最佳选择吗?
近日,开源 LLM 网关项目 Bifrost 的维护者在 Reddit 上分享了一篇题为《Why we chose Go over Python for building an LLM gateway》的技术复盘,引发了社区的强烈反响。他们放弃了拥有 LiteLLM 等成熟竞品的 Python 生态,转而使用 Go 重写了核心网关。结果令人咋舌:延迟降低了约 700 倍,内存占用降低了 68%,吞吐量提升了 3 倍。
这场技术选型的背后,折射出的是 AI 工程化进入深水区后,对并发模型、资源效率与部署架构的重新审视。

在项目的初期,选择 Python 似乎是理所当然的。
1. 生态惯性与“胶水”优势
绝大多数 AI 工程师都是 Python Native。从 LangChain 到 LlamaIndex,几乎所有的 Agent 开发框架都优先支持 Python。使用 Python 构建网关,意味着可以直接复用现有的库,甚至可以直接挂载一些轻量级的 Python 逻辑来处理 Embeddings 或 RAG(检索增强生成)流程。FastAPI 的易用性更是让开发者能在几分钟内搭建起一个服务。
2. 遭遇瓶颈:网关的本质是 I/O
然而,LLM 网关的业务属性决定了它的性能痛点。与计算密集型(CPU-bound)的模型推理不同,网关是典型的 I/O 密集型应用。它的核心职责是:
在这个过程中,网关绝大部分时间都在“等待”。
3. Python 的并发痛点
Bifrost 团队在测试中发现,当并发请求数达到 500-1000 RPS(每秒请求数)时,Python 的瓶颈开始显现。
Bifrost 团队最终选择了 Go。这一决定并非出于对语言的偏好,而是基于冷冰冰的 Benchmark 数据。让我们深入分析他们披露的核心指标。

数据对比:
* Bifrost (Go): ~11 微秒 (0.011ms) / 请求
* LiteLLM (Python): ~8 毫秒 / 请求
这是一个惊人的 700 倍 差距。
虽然 8 毫秒在人类感知中似乎微不足道,但在高并发架构中,这被称为“开销放大”。
此外,Go 的 net/http 标准库在处理 HTTP 请求时经过了极致优化。Go 不需要像 Python 那样依赖 ASGI/WSGI 服务器(如 Uvicorn),其原生的 HTTP 处理能力配合 Goroutine,使得每个请求的内存分配和 CPU 周期都降到了最低。
架构对比:
* Go: 10,000 个 Goroutines,每个仅占用 ~2KB 栈空间。
* Python: 受限于 OS 线程开销或 Event Loop 的单核瓶颈。
LLM 网关的特殊性在于长连接。LLM 的流式输出可能持续数秒甚至更久。这意味着网关必须同时维护成千上万个活跃连接。
Go 的 GMP(Goroutine-Machine-Processor)调度模型天生适合这种场景。成千上万个 Goroutine 可以复用少量的系统线程,上下文切换由 Go Runtime 在用户态极速完成,几乎不消耗系统内核资源。
相比之下,Python 即使使用了 uvloop,在面对海量并发连接的数据搬运时,其解释器的开销依然是一个沉重的包袱。
数据对比:
* Go: 内存占用降低 ~68%。
* 生产环境: Go 跑在 t3.medium (2 vCPU, 4GB) 上即可;Python 则需要 t3.xlarge。
对于大规模部署 AI 服务的企业来说,这意味着基础设施成本直接减半。
Python 的动态类型系统和垃圾回收机制导致其对象内存占用较大。而 Go 的结构体布局紧凑,且编译器能进行逃逸分析(Escape Analysis),将大量对象分配在栈上而非堆上,从而显著降低了 GC 压力和内存占用。
这篇帖子在 r/golang 引发了极高质量的讨论,评论区揭示了行业内更深层次的趋势。
过去,Python 的一大优势是“开发效率高”。写 Python 代码通常比写 Go 或 Rust 快。
但在 2026 年,“Agentic Coding”(即利用 AI Coding Agent 辅助编程)已经成为主流。
有开发者指出:“LLM 让编写 Rust 和 Go 变得非常高效,你完全可以享受到高性能语言的红利,而不用支付编写它们的‘学习成本’。”
这是一个极其深刻的洞察。
当“编写代码”不再是瓶颈时,“运行时性能”和“稳定性”的权重就被无限放大了。这进一步削弱了 Python 在后端基础设施层的竞争力。
既然要高性能,为什么不直接上 Rust?
评论区对此展开了激辩。虽然 Rust 在理论上拥有比 Go 更高的性能上限和内存安全性(无 GC),但 Go 在“开发效率”与“运行效率”之间找到了完美的平衡点。
对于大多数 AI 网关场景,Go 是性价比最高的选择。
这是否意味着 Python 将被淘汰?绝不。
社区共识非常明确:Python 的护城河在于 ML 生态。
但在生产环境部署(Production Serving)阶段,架构正在发生分离:
Bifrost 的案例只是冰山一角。我们正在目睹 Go 语言在 AI 领域的“新基建”运动。
Deployment simplicity 是作者提到的另一个关键点。
部署 Python 应用通常意味着:配置 Docker -> 安装 Python -> pip install requirements.txt -> 解决依赖冲突 -> 虚拟环境管理。
而部署 Go 应用:COPY bifrost /usr/local/bin/ -> Run。
在容器化和 K8s 盛行的今天,Go 的静态链接二进制文件极大地简化了 CI/CD 流程,减小了镜像体积,提升了冷启动速度(这对于 Serverless AI 推理尤为重要)。
虽然 Go 在 Tensor 操作库上不如 Python 丰富,但在应用层工具上正在迅速补齐。
如果你正在构建一个 AI 应用平台:
Bifrost 从 Python 到 Go 的迁移,不仅仅是一次代码重写,更是一次架构理念的升级。它证明了在 AI 浪潮中,基础设施的性能与模型的智能同等重要。
随着 LLM 应用规模的爆发式增长,计算成本和响应延迟将成为企业关注的焦点。Go 语言凭借其高效的并发模型、极低的资源占用和极简的部署体验,正在成为 AI 基础设施层的“事实标准”。
对于 Gopher 而言,这是一个最好的时代。我们不需要成为算法专家,只需要发挥 Go 语言最擅长的能力——构建高性能、高可靠的管道,就能在 AI 时代占据不可或缺的一席之地。
资料链接:https://www.reddit.com/r/golang/comments/1r27pqx/why_we_chose_go_over_python_for_building_an_llm/
你认为 Python 会被“边缘化”吗?
随着 Agentic Coding 的普及,高性能语言的入门门槛正在消失。在你的 AI 实践中,是否也感受到了 Python 在生产部署时的无奈?你认为 Go 在 AI 领域还会攻下哪些阵地?
欢迎在评论区分享你的看法!
还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:
扫描下方二维码,开启你的AI原生开发之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

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

© 2026, bigwhite. 版权所有.