
大家好,我是Tony Bai。
如果有人问你:在处理纯 CPU 密集型的文本匹配时,Go 和 Python 哪个快?
相信 99% 的 Go 开发者会毫不犹豫地把票投给 Go。毕竟,一门编译型的静态语言,怎么可能输给拖着 GIL 锁的解释型脚本语言(注:新版本Python已经接触了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 的标准库在“安全、内存、性能”的三角博弈中,选择了妥协于现状。
coregex官方的克制固然令人敬佩,但对于身处一线的业务开发者来说,由于正则太慢导致的 CPU 告警,是实实在在的痛点。
“既然官方不愿意改,那我们就自己造轮子!”
在近期的 Issue #26623 中,一位名为kolkov的开发者带着他的开源库coregex杀入了战场,向 Go 标准库发起了直接的挑战。
coregex是一个完全用纯 Go 编写的正则库,它的出现直接将 Go 的正则性能拉到了与 Rust 并驾齐驱,甚至在某些场景下超越 Rust 的境地。
它是怎么做到的?它在底层祭出了几个大杀器:
.*\.txt这种正则,速度直接飙升了1500倍!coregex通过切片状态共享,让内存分配直接减少了 50%。在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% 的惨痛经历?你又是如何排查和优化的?
欢迎在评论区分享你的血泪史👇
如果本文对你有所帮助,请帮忙点赞、推荐和转发
!
点击下面标题,阅读更多干货!
- 揭秘Go语言中的rune:一段跨越30年的Plan 9往事与UTF-8的诞生传奇
- 从 Python 到 Go:我们失去了什么,又得到了什么?
- 拉个 JSON 居然要装 5 个第三方库?终于明白 Go 的标准库到底有多“霸道”
🔥 还在为“复制粘贴喂AI”而烦恼?我的新极客时间专栏《AI原生开发工作流实战》将带你:
扫描下方二维码👇,开启你的AI原生开发之旅。
