本文永久链接 – https://tonybai.com/2026/10/12/turborepo-go-to-rust-agent-era-calculus

大家好,我是Tony Bai。

【导读】

Vercel CEO Guillermo Rauch 最近抛出一条长推文,一句话引爆技术圈:Turborepo 从 Go 迁移到 Rust 的工作完成了,可它的投入产出比,在公司内部竟然“相当有争议”!Go 很快、设计优雅、易于迭代,Rust 更适合底层系统,可真正让团队纠结的,是“由人类来写代码”的迁移成本。而在 AI Agent 的“超音速海啸”面前,这道题的计算方式已经变了。本文带你回到 2023 年的技术现场,完整还原这场迁移的决策、策略与踩坑,再聊聊它对今天每一个工程团队的启示。

【文章要点】

  • 背景:Rauch 在推文中表示,Turborepo 从 Go 到 Rust 的迁移已完成,但 ROI 在内部“相当有争议”。
  • 为什么迁:Go 的设计优先级(简洁、易迭代、适合数据中心服务)与 Turborepo 的需求(用户机器上的底层 OS 交互、编译期正确性)存在错位。
  • 怎么迁:选择“增量移植”而不是“推倒重写”,先用一个全新的小功能(global turbo)打通 Rust 工具链,再逐步迁移 CLI 解析等模块。
  • 踩坑:Windows 工具链与 UNC 路径、Alpine Linux 上的 glibc 与 musl 问题,最终把 Go 与 Rust 拆成两个二进制,通过 JSON 传参。
  • 新变量:Rauch 认为“计算公式变了”,“对人类最好”未必还是“对业务最好”,而且 Rust 也不会是终点。


一条推文,几十万次浏览:Rauch 到底说了什么

10 月 5 日,Vercel CEO Guillermo Rauch(网名 @rauchg)发了一条长推文,截至截图时已有近 40 万次浏览。

推文开头,他先表态:DHH 在 Rust 这件事上“根本上是对的”。随后,他抛出了 Vercel 内部的一手经验。把要点翻译整理一下:

  • Vercel 已经“Rust 化”(原文用了 carcinization,即“蟹化”,Rust 的吉祥物正是螃蟹)有一段时间了;
  • 最早迁移的项目之一就是 Turborepo,从 Go 迁到 Rust;
  • 迁移完成了,但 ROI(投资回报率)在内部相当有争议;
  • 在他们看来,Rust 更适合低层 OS 访问,这对 Turbo 这样的构建系统至关重要,但“人类迁移成本”非常高;
  • Go 很快、设计漂亮、易于迭代。团队非常纠结,因为当时是人类在写代码,“即使我们早就知道 Rust 是更好的选择”;
  • 如今,计算公式变了:“对人类最好”不再必然等于“对业务最好”;
  • 最后一句更有意思:Rust 不太可能是终极工具链,“更绿的牧场就在前方”,因为 Rust 诞生于 Agent“超音速海啸”之前。

一个技术决策已经落地,CEO 却站出来说:这笔账有争议。这在技术圈并不常见。

要读懂这条推文,我们得先回到 2023 年,看看这次迁移当初是怎么决策、怎么执行的。Vercel 当年写过两篇博客,把“为什么迁”和“怎么迁”交代得相当详细。

先放一张 Turborepo 从 Go 到 Rust 的关键节点时间线:

  • 2023 年 3 月:发布《为什么迁移》:宣布从 1.7 版本开始增量迁移
  • 2023 年 7 月:发布《怎么迁移》:总结增量移植策略与跨平台踩坑
  • v1.7.0:首个 Go 与 Rust 混合版本发布
  • 2026 年 10 月:Rauch 发推复盘:迁移已完成,但 ROI 存在争议

时间回到 2023:Turborepo 为什么要离开 Go

Go 的优先级,与 Turbo 的需求错位了

很多人以为 Turborepo 迁移 Rust 是“跟风”,其实当初选 Go 本身就是有理有据的。

Turborepo 最早选择 Go,是追随 esbuild 的脚步。esbuild 是用 Go 写的 JavaScript 打包器,速度快,还避开了 Node.js 的大部分初始化开销。再加上 Go 的开发体验天然适合快速迭代,而早期的 Turborepo 恰恰需要不断摸索开发者到底想要什么。

那么问题出在哪里?官方的说法是:随着代码库规模变大,并与 Turbopack 合并,Go 开始在最关键的地方“服务不了”团队和用户。

Vercel 的分析很坦诚,核心就是一句话:语言的设计优先级与我们的优先级不一致。

  • Go 的强项是数据中心里的网络计算:goroutine-per-request 模型、Context API、标准库自带服务端基础设施,都体现了这一点;
  • Go 偏好简洁而非表达力,副作用是更多错误要留到运行时才暴露;
  • 如果是跑在数据中心的服务,出了问题可以回滚、修复、再上线,代价可控;但 Turborepo 是装在用户机器上的软件,每一个错误的成本都更高。

而 Rust 的社区更看重“正确性优先于 API 抽象”。在进程管理、文件系统、底层 OS 概念、向用户机器分发软件这些场景里,这恰好是 Turbo 最在意的东西。

一个小例子:文件权限

官方给了一个非常直观的例子:文件权限。

Go 允许你设置 Unix 风格的权限码(比如 rw-r--r--)。听起来很方便,但这个抽象无法跨平台,因为 Windows 根本没有这种精确的文件权限概念。结果就是,Go 允许你在 Windows 上设置权限码,哪怕它毫无作用。

Rust 则要求你显式声明:这段代码只在 Unix 上生效。如果不声明,代码在 Windows 上根本编译不过。下面是一个示意(非 Turborepo 源码):

// 示意代码:只在 Unix 平台编译,其他平台直接编译失败或被排除
#[cfg(unix)]
fn make_executable(path: &std::path::Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
}

官方的评价是:把复杂度暴露出来,让我们在软件交付给用户之前,就知道代码到底在做什么。

CGO 的“全局”代价与 Turbopack 的重复劳动

除了设计理念,还有两个非常现实的因素:

第一,CGO 的“全局”代价。

Turborepo 越来越多地依赖 C 原生库,比如用来压缩缓存文件的 zstd。在 Go 里调用 C 库要用 CGO,这会让团队从纯 Go 工具链切换到慢得多的 C 工具链,而且这个切换是全局性的:只要用了一个原生库,整个代码库都得用 CGO 构建。

在 Rust 里,这件事要“克制”得多。bindgen、cxx 等库可以生成安全封装,不需要全局修改构建方式。比如他们用 git2 这个 crate 迁移了 git 接口,它底层调用 C 库 libgit2,对外却是安全、地道的 Rust API。

第二,Turbopack 团队早就在用 Rust。

两个团队共用代码库、紧密协作,却在解决同样的问题:一次用 Go,一次用 Rust。统一语言之后,双方可以共同维护通用工具,比如借鉴 Turbopack 的文件监听方案,更快做出跨工作区的智能热更新。

顺带一提,官方还列了一条“软理由”:团队想写 Rust,开发者更开心、更不容易倦怠;而且从历年 StackOverflow 调查看,Rust 的用户满意度一直很高,也是 Web 开发者学完 JavaScript 之后最想学的第二门语言之一。

不推倒重写:增量移植的策略

决定要迁,接下来的问题是:怎么迁?

增量移植 vs 完全重写

“推倒重来”永远很诱人:没有历史包袱,不用考虑新旧代码共存,写起来也简单。但 Vercel 评估后认为,完全重写有三个硬伤:

  1. 需要暂停新功能开发,否则就会追着一个不断变大的旧代码库跑;
  2. 不保证更好的用户体验,新版本很难做到和旧版“功能对功能、边界情况对边界情况”完全一致,用户可能被各种破坏性变更和功能缺失折腾;
  3. 会产生大量“未被使用”的新代码,而哪怕有测试覆盖,没被真实使用的代码也是 bug 的温床。

所以他们选择了增量移植:一块一块把代码搬到 Rust,让新旧代码同时运行,要求被移植的那一块代码行为与之前完全一致,只做“翻译”,明确不做改进、不改功能。这样才能用同一套测试去对照新旧实现,尽快完成迁移。

当然,代价也很实在:为了让 Go 与 Rust 互操作,代码库里引入了大量额外复杂度,起步阶段开发速度会变慢。但换来的是迁移期间仍然可以持续给用户交付功能。

第一枪:用 global turbo 打通 Rust

怎么开始?他们的思路很聪明:先用 Rust 写一个全新的小功能,既能交付路线图上的内容,又能把 Rust 集成进构建流程,同时尽量少碰现有的 Go 代码。

这个功能就是 global turbo:允许用户把 turbo 安装成全局命令。它会优先查找仓库里本地安装的 turbo 并执行,找不到再回退到全局二进制,这样既能在仓库任意目录下运行,又能在 package.json 里锁定版本。

实现方式是一个被称为 “Rust shim” 的薄薄一层 Rust 代码,包在原有 Go 代码外面。Go 部分通过 CGO 编译成 C 静态库,再链接进 Rust 二进制。由于 global turbo 只需要读取配置、遍历文件系统等少数能力,所以很快就打通了。

后来官方把这个架构戏称为 “Rust-Go-Rust 三明治”:Rust 是入口,决定某个命令由 Rust 还是 Go 实现;Go 也可以反过来调用 Rust,给团队留出一条始终能走向 Rust 的路径。

CLI 解析与“JSON 过桥”的 FFI 设计

做 global turbo 的过程中,他们发现需要解析 --cwd 之类的命令行参数,于是顺势把整个 CLI 参数解析也搬到了 Rust,用的是 clap 这个 crate。

接下来的问题是:怎么把解析好的参数从 Rust 入口传给 Go?

FFI(外部函数接口)的事实标准是 C,但团队担心写不好“既能和 Rust 配合、又能和 Go 配合”的跨平台 C 类型。于是他们选了一个很“朴素”的办法:把参数序列化成 JSON 字符串传过去。

  • Rust 侧用 serde 序列化;
  • Go 侧本来就在用 JSON,直接反序列化成结构体;
  • 参数结构体只有几百字节,JSON 的性能开销可以忽略。

示意代码如下(非 Turborepo 源码):

use serde::Serialize;

#[derive(Serialize)]
struct Args {
    cwd: Option<String>,
    command: String,
    // ... 其他参数
}

// 序列化为 JSON 字符串,再交给 Go 侧
let payload = serde_json::to_string(&args)?;

这个看似“土”的设计,后来在遇到大麻烦时救了他们一命。

踩坑实录:真正难的是跨平台发布

两个功能迁完,团队准备发布第一个 Go 与 Rust 的混合版本。然而在不同平台上,问题接踵而至。

Windows:MSVC / MinGW 与 UNC 路径

第一个坑:工具链不一致。

Windows 上有两大工具链:MSVC 与 MinGW。Go 只使用 MinGW,而 Rust 这边用的是 MSVC,结果出现运行时问题。解决办法很简单:把 Rust 工具链也切到 MinGW。

第二个坑:UNC 路径。

在 Windows 上对路径做规范化(解析符号链接、标准化路径各部分)时,系统会返回 UNC(通用命名约定)路径。可名字虽叫“通用”,它却并非处处受支持,有时连 Windows 自己都不认,导致传入 UNC 路径后报“无效路径”。

解决办法是用一个叫 dunce 的 Rust crate:规范化路径的同时,返回非 UNC 形式的路径,把其中的细节都处理掉。

Alpine:glibc、musl 与那个 7 年前的 issue

更硬的骨头出在 Alpine Linux 上。Vercel 用 Alpine 来构建轻量级容器,它是云计算里非常常见的系统。

Alpine 没有 glibc,而很多二进制默认假定系统里装了 glibc。gcompat、libc6-compat 这类兼容层也没救成,因为 Rust 所需的 glibc 版本,对他们支持的目标平台来说太新了。

于是他们决定把 Turborepo 编译成完全静态的二进制,用 musl 打包 C 标准库(glibc 因为许可问题无法静态链接)。Rust 可以直接切换目标,Go 默认也不使用 C 标准库,理论上一切顺利。

可实际一跑,段错误。用调试器一看,栈被破坏了。更离谱的是,崩溃看起来来自 Go 运行时本身。

团队翻了很久,终于找到一个已存在 7 年的 GitHub issue:Go 无法与 musl 一起被编译成 C 静态库。这意味着,Alpine 这个关键平台,眼看就要被“卡死”。

最终方案:两个二进制

怎么办?答案就是前面那个“土”设计的回报:

把 Go 和 Rust 编译成两个独立的二进制,由 Rust 通过命令行,把序列化成 JSON 的参数传给 Go。

因为之前已经把 FFI 的接触面压到最小,只是一段 JSON,这次切换的代码改动极小:只需要改 Rust 向 Go 发送 JSON 字符串的方式。

Turborepo 的首个混合发布版本,就是 v1.7.0。

官方复盘的四条经验

在 2023 年的博客里,Vercel 自己总结了四条经验。放到今天回看,每一条都很扎心:

  1. 序列化是 FFI 的好朋友。用 JSON 这种两种语言都有成熟支持的格式,可以把 FFI 的接触面压到最小,避免一整类跨平台、跨语言的 bug。代价是序列化有开销,只适合载荷很小或不在意性能的场景。
  2. 移植需要充分准备。增量移植可行,但需要大量谨慎的测试和策略。CLI 解析的边界细节、配置的加载顺序,这些在写第一版时无关紧要,但在移植时一个都不能错。官方建议:在移植代码之前,就先把测试写好,让它成为你的“规格说明”。
  3. 跨兼容性很难。跨平台、跨语言的发布工程极具挑战。每个平台、语言、编译器都有自己的“脾气”,参与的东西越多,出新麻烦的机会就越多。
  4. 对我们来说,移植是值得的。即便过程中遇到了极其棘手的调试问题,他们依然在迁移期间持续交付功能、修复 bug。当时的结论是:值得。

2023 年的结论是“值得”。可到了 2026 年,CEO 却说“ROI 相当有争议”。这中间发生了什么?

2026 年的新账本:“计算公式”变了

Rauch 的核心论点

把 Rauch 的推文和 2023 年的博客放在一起读,你会发现一个很有意思的视角转换。

2023 年的账,是这样算的:

  • 收益:编译期正确性、底层 OS 能力、与 Turbopack 共享生态、团队更开心;
  • 成本:互操作的复杂度、跨平台发布难题、开发速度变慢;
  • 结论:为了长期收益,人类愿意付出短期成本。

2026 年,Rauch 的表述变成了:

  • Rust 在技术上更适合;
  • 但人类写代码让迁移成本非常 substantive(原文用词);
  • Go 又快、又美、又好迭代,“即使我们知道 Rust 更好”,也拿不定主意;
  • 而今天,“对人类最好”不再必然等于“对业务最好”。

换句话说:过去,选语言的一个核心约束,是人类程序员的体验与效率。语言要好读、好写、好迭代,因为写代码的是人。而当写代码的主体越来越多地变成 AI Agent,这个约束就松动了。

为什么 Agent 会改变迁移的账

我们可以把语言选型和迁移的账本,拆成几项来看看哪些在变:

第一,迁移成本的结构在变。

2023 年博客里最耗人力的部分:逐模块“翻译”、对照行为、补测试,恰恰是 Agent 比较擅长的重复性工作。人类迁移成本高,意味着在 Agent 时代,“推迟迁移”本身可能就是一种隐性成本。

第二,语言的“人体工学”权重下降。

Go 的优势之一是易于迭代、设计优雅,这些主要是对人类有价值的特性。而 Rust 的编译期约束、类型系统,对人类来说是学习曲线,对 Agent 来说却可能是天然的护栏,因为编译器会替你拦下一大批错误。

第三,“新语言”的评判标准可能会变。

Rauch 最后一句话的潜台词是:Rust 是为“人类写代码”设计的,当 Agent 成为主要的写作者,下一代语言或工具链会按照新的约束重新设计。这是一个判断而非定论,但值得工程团队留意。

冷静一点:三个不要误读的地方

热点话题最容易被“一刀切”解读,这里提醒三点:

1. 这不是说 Go 不行。

Rauch 自己就说 Go“非常快、设计优美、易于迭代”。Go 在网络服务、云基础设施领域的优势并没有改变。Turborepo 迁移的原因,是场景特殊:在用户机器上运行的构建系统,需要精细的 OS 交互与编译期保证。

2. “迁移成本降低”不等于“迁移成本为零”。

2023 年的经验里,最难的是跨平台发布工程:MinGW、UNC 路径、musl 与 Go 运行时的冲突,这些问题靠“翻译代码”解决不了,需要真实环境里的调试与取舍。再强的 Agent,也得有可靠的验证体系兜底,所以“先写测试,再迁移”的建议只会更重要,而不是更不重要。

3. “更绿的牧场”是预判,不是路线图。

Rauch 说他“相当确定”会有更好的选择,但并没有指出具体是什么。在新工具链真正出现之前,用好当下最合适的语言依然是最稳妥的策略。

小结

回头看,这是一个非常有意思的“双时间线”故事:

2023 年,Vercel 工程师们用 JSON 和两个二进制,绕过了一个存在 7 年的 Go 与 musl 之间的 issue,把 Turborepo 稳稳地推向了 Rust。

2026 年,CEO 回过头来说:迁移做完了,但如果放在今天,这笔账可能会有不同的算法。

这并不是对过去决策的否定,而是一个提醒:我们正处在“谁来写代码”这个基本假设被改写的时刻。当它变了,很多过去“理所当然”的工程取舍,都值得重新算一遍。

至于 Rust 是不是终点?也许 Rauch 说得对:更绿的牧场,还在前面。🦀


参考资料

  1. Guillermo Rauch(@rauchg)推文:https://x.com/rauchg/status/2106863842450133114
  2. Vercel 博客:Why Turborepo is migrating from Go to Rust(2023 年 3 月 7 日):https://vercel.com/blog/turborepo-migration-go-rust
  3. Vercel 博客:How Turborepo is porting from Go to Rust(2023 年 7 月 21 日):https://vercel.com/blog/how-turborepo-is-porting-from-go-to-rust
  4. Go issue(Go 无法与 musl 一起编译为 C 静态库):https://github.com/golang/go/issues/13492

还在为“复制粘贴喂AI”而烦恼?我的新专栏 《AI原生开发工作流实战》 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


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

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

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

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

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


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