大家都在说 Go 不适合做底层基础设施,都在说 C 语言的地位不可撼动。

image

直到有人把 Tree-sitter——这个被誉为"解析器界瑞士军刀"的 C 语言项目——完整移植到了 Go。

结果基准测试跑出来,所有人都沉默了:全量解析快 1.5 倍,增量解析快 90 倍,而在无编辑重解析的场景下,纯 Go 版本甚至比原版 C 快了 14000 倍。这哪里是移植?这简直是把原版按在地上摩擦。

"C 一定比 Go 快"的神话破灭了

说实话,看到这个数据的第一眼,我也以为是测试人员把小数点点错了。

但事实就是如此荒诞。在 BenchmarkGoParseIncrementalNoEdit 测试中,纯 Go 版本耗时 8.63 纳秒,而通过 CGo 调用原版 C 库耗时 121,100 纳秒

差了四个数量级。

为什么会这样?难道 Go 的代码写得比 C 还好?其实原因很朴素,也很残酷:CGo 的调用开销太大了

原版 Tree-sitter 是 C 写的,Go 要用它就得通过 CGo 绑定。每次调用都得穿越 Go 运行时和 C 运行时的边界,这一来一回,时间全花在了"过路费"上。

而纯 Go 版本呢?

增量解析的热路径激进地重用子树,单字节编辑只需微秒级。无编辑的快速路径更绝,直接在一个 nil 检查上退出——零内存分配,个位数纳秒。

没有边界,没有开销,只有纯粹的逻辑。

摆脱 CGo,是多少开发者的梦想

老实讲,CGo 一直是 Go 开发者心中的痛。

你想用一个强大的 C 库,就得接受 CGo 带来的复杂性:编译环境难搞、交叉编译困难、部署体积膨胀、性能损耗……

image

这就像你想吃顿好的,结果发现得先自己种菜、养猪、搭灶台。

这也是为什么 gotreesitter 这个项目一出,评论区立马炸了锅。

有开发者兴奋地表示,这对 Bazel 社区简直是天大的好消息。

因为 Gazelle 语言扩展是用 Go 写的,以前想依赖 Tree-sitter 就得用 CGo,现在终于可以摆脱这个"诅咒"了。

还有人提到,对于像 Gitea、Forgejo 这种基于 Go 的代码托管平台,这东西简直是语法高亮的神器。

"我有严格的 no-cgo 要求,所以可能会在我的项目里用这个。"

你看,有些时候,"纯 Go"不仅仅是一种技术选型,更是一种信仰。

205 种语法,一场硬核的"搬运战"

移植 Tree-sitter 可不是简单的翻译工作。

Tree-sitter 最强大的地方在于它支持海量的编程语言语法。要移植它,就意味着要把这些语法的解析逻辑全部搬过来。205 种语法

从 Ada 到 Zig,从 C 到 Rust,甚至包括那些你听都没听过的语言,比如 Beancount、Cooklang、Norg。

作者不仅要移植核心解析器,还要处理大量的词法分析器。其中 195 个使用了 DFA(确定性有限状态自动机)后端,还有 9 个需要手写纯 Go 的词法分析器桥接

image

最麻烦的是外部扫描器。

有些语言的语法特别刁钻,通用的解析器搞不定,就得手写代码去处理。作者硬是为 111 种语言手写了 Go 版本的外部扫描器。这工作量,想想都让人头皮发麻。

并不完美,但足够惊艳

当然,这世界上没有完美的项目。

在评论区里,除了叫好声,也夹杂着一些冷静的质疑。

有人指出,这个移植版缺少 TreeCursors 和 tree-sitter-generate。

TreeCursors 是一种高效的树遍历 API,没有它,有些场景下查询和遍历都不太顺手。而 tree-sitter-generate 更是关键,它能让你动态生成和编译语法,这对于某些需要运行时扩展的用户来说,是刚需。还有人注意到了 "partial" 模式的问题。

目前有一个语言——norg——是部分支持的,因为它的外部扫描器还没实现。这导致在某些语法下,解析结果可能不正确。

"为什么你会有一种保证在某些语法下输出错误的解析模式?"

这个问题挺尖锐。

不过,考虑到这是一次"从零开始"的移植,支持了 205 种语言中的 204 种完整解析,这成绩单已经足够傲人了。

名字是个大问题

最后,聊聊那个有点尴尬的小插曲。有人建议项目名叫 got。

结果立马有人跳出来反对:这不就跟 OpenBSD 的 Got(Game of Trees)撞车了吗?

在开源世界里,起名是个技术活。叫 gotreesitter 虽然有点长,但至少安全,不会引起混淆。

有意思的是,还有人提到之前也有人用 Rust 重写过 Tree-sitter,但似乎对最终用户来说,用处没那么大。

技术移植这事儿,光有技术还不够,还得找准生态位的痛点。Go 生态对 CGo 的"嫌弃",恰恰给了 gotreesitter 最大的生存空间。

结语

14000 倍的性能提升,或许只是极端场景下的个例。

但它揭示了一个趋势:随着语言 runtime 的进化,"C 一定快"的刻板印象正在瓦解

当 Go 摆脱了 CGo 的枷锁,它所爆发出的能量,足以让任何一个 C 语言老手侧目。

不过,我也想问一句:如果未来 WASM 成了主流,这种"原生移植"的故事,还会继续上演吗?

参考链接:
https://github.com/odvcencio/gotreesitter