为什么 Turborepo 从 Go 迁移到 Rust


Turborepo是一个用于 JavaScript 和 TypeScript 代码库的高性能构建系统。我们正在重新构想构建系统,从 Buck 和 Bazel 等工具中汲取灵感,让每个人都能使用它们。Turborepo 的核心是一个非常简单的想法:永远不要做同样的工作两次。我们通过增量构建、并行执行和远程缓存来实现这一点。

随着使用量的增长和产品需求的转变,我们决定在1.7 版本中开始从 Go 到 Rust 的增量迁移。在本文中,您将了解我们进行此迁移的动机以及我们发现 Rust 为我们的团队解决的问题。

Turborepo 使用 Go 的最初决定是在esbuild. 作为一个用 Go 编写的 JavaScript 打包器,esbuild速度很快并且避免了 Node.js 的大部分初始化开销。此外,Go 的开发人员体验是为迭代量身定制的,随着我们越来越多地了解开发人员希望从turbo.
在 Turborepo 的早期,Go 的这些特性恰好为我们提供了项目成功所需的条件。然而,随着Turborepo 代码库的扩展并与Turbopack合并,Go 开始在对 Turbo 最重要的领域为我们的核心团队和用户提供服务不足。


Go 的语言设计优先级
Go 的优势在于数据中心的网络计算,它擅长这项任务,为世界上最大规模的这些工作负载提供支持。服务器基础设施的 goroutine-per-request 模型、上下文 API 和标准库包含证明了这种社区关注。

此外,Go 更喜欢简单性而不是表现力。该决定的副作用意味着在运行时会捕获更多错误,而其他语言可能会在编译时捕获它们。通过在数据中心运行的服务,您可以在方便时回滚、修复和前滚。但是,在构建用户安装的软件时,每个错误的成本都更高。

对我们来说,使用优先考虑前期正确性的工具是值得的。我们充分认识到Go 的优先级我们正在优先考虑的问题不匹配,这是我们为自己创造的要解决的问题。


我们的需求与 Rust 保持一致
Rust 语言和社区将正确性置于 API 抽象之上——这是我们在使用时非常关心的权衡:

  • 流程管理
  • 文件系统
  • 其他低级操作系统概念
  • 将软件运送到我们用户的机器上

这意味着我们的代码库中出现了额外的复杂性,但对于我们试图解决的问题来说,这是必要的复杂性。
Rust 的类型系统和安全功能使我们能够在我们需要的代码库中放置防护栏。该语言的表现力使我们的开发人员能够对在编译时而不是在 GitHub 问题中捕获错误的约束进行编码。

举例比较:文件权限
Go 对文件系统简单性的偏好在文件权限方面给我们带来了问题。Go 允许用户设置一个 Unix 风格的文件权限代码:一个描述谁可以读取、写入或执行文件的简短数字。

> ls -l turbo.json
-rw-r--r--  1 anthonyshew  users  247 Jan 1 00:01 turbo.json

虽然这听起来很方便,但这种抽象不适用于跨平台;Windows 实际上并没有文件权限的精确概念。Go 最终允许我们在 Windows 上设置文件权限代码,即使这样做没有任何效果。
相比之下,Rust 在这方面的明确性不仅使我们的事情变得更简单,而且也更加正确。如果你想在 Rust 中设置文件权限代码,你必须明确地将代码注释为 Unix-only。否则,代码甚至无法在 Windows 上编译。这种复杂性的表象有助于我们在将软件交付给用户之前了解我们的代码在做什么。

Rust 拥有一个由高质量、开源 crate(Rust 相当于 npm 包)组成的梦幻般的生态系统,它们明确关注我们关心的事情。我们从这种对齐中受益的一个例子是当我们必须与用 C 或 C++ 编写的本机库进行交互时。

现在,我们有了所谓的“Rust-Go-Rust Sandwich”。Rust 是入口点,允许我们选择特定命令的实现是 Rust 还是 Go。我们的 Go 代码也能够调用 Rust 代码,为我们提供了保持 Go 运行但始终能够到达 Rust 的路径。