【外评】一年的 Rust 开发总结

在过去的几个月里,我(几乎)把所有的空闲时间都花在了我的副业 JustFax 上。一切都从从 LemonSqueezy 迁移到 Stripe 开始(有兴趣知道为什么吗?请订阅我其他博客的时事通讯,因为我计划撰写一篇详细的分析文章,说明为什么要选择支付网关而不是记录商户)。但就像每次重构或重写一样,它的发展远远超出了我的预期。一个简单的支付提供商变更,就要求我在 SQL 的基础上实现一个作业处理队列,还要做一个小小的会计系统。当然,所有这些都是在 Rust 中完成的。这是我合并过的最大的合并请求之一。

这个合并请求来得正是时候,它提醒我,我在一年前(也许是 11 个月前)就开始开发 JustFax 了。除此之外,我还在我的 $MAIN_JOB 中使用 Rust 进行生产,因此我决定概述一下在生产中使用 Rust 的情况。

如果编译成功,就能运行

我在用 Rust 编写网络应用程序的最初印象中简单提到过这一点,我想再次强调这一点。

通常情况下,我不会在修改每一行代码后都运行代码(除非我做了恼人的 CSS 调整)。大多数时候,我会完成一大块重构或功能实现,然后运行所有代码进行测试。此外,使用 Rust,你无法运行未编译的代码,所以如果你的代码有错误,你需要修复它们,这一点与 JavaScript 不同。就这样,我忘我地投入到重构工作中,在几个月的时间里,我度过了无数个夜晚。事情一件接着一件,代码始终无法编译,直到最后一刻才全部完成。

在运行所有程序时,我感到非常害怕,但在设置好系统的每个组件并发出第一个(本地)请求后,我惊讶地发现一切正常!我知道自己是个优秀的开发人员,但我不会自欺欺人地认为自己编写的代码不会出错。除了代码动态方面的小问题,比如错误的序列化/解序列化格式,代码工作得非常完美。

成功的第一层原因是 Rust 的类型安全和编译特性。Rust 编译器不会让你意外地将 i64 赋值给 uuid。你也不能访问结构体中不存在的字段,这与 JavaScript 不同。当然,也有人声称自己从未遇到过无法访问属性 “foo ”的未定义运行时错误,但我和我同事的经历并不相同。我们在我的 $MAIN_JOB 中使用 Rust 的原因之一,就是因为用动态语言编写的现有后台变得难以管理。当然,你可以在脑海中保存一个小型应用程序的上下文,但总有一天它会超出你的记忆范围。我记得以前做 PHP 的时候,为了知道数组有哪些键,我们不得不在屏幕上显示数组的内容,因为有些键是 snake_case,而有些键是 camelCase,等等。

第二层,也算是第一层的延伸,是 Rust 社区中人们对类型安全的痴迷。这是一种很好的痴迷,因为它催生了 sqlx 这样的工具–一个编译时类型安全的 SQL 封装器,它可以针对真实数据库运行你的查询,如果你的查询语法错误,或者你试图在 TEXT 列中插入一个 i32 而没有显式转换,你的代码将无法编译。我曾经很害怕 SQL,因为它通常是每个应用程序的重要组成部分,而且很容易出错。如果写错查询或弄乱参数,那就祝你好运了。许多人会围绕 SQL 查询编写单元测试,而其他人则会使用 ORM 或从模式中生成代码。但有了 sqlx,我再也不用担心了。我无需使用高度抽象的 ORM,也无需从数据库模式中生成代码,就能获得 SQL 的全部功能。当然也有缺点,我会在博文中进一步提及。

另一种工具是类型安全模板引擎,如 askama 或 maud。虽然在我的项目中没有找到它们的用武之地,但它们还是消除了代码中的另一个不可预测性因素。让我发笑的一件事是,当我收到一封以 “你好,{{userName}}”开头的电子邮件时。从无名服务机构收到这种邮件很有趣,但从银行收到就很尴尬了。有了类型安全的编译模板,这种错误就不存在了。

我唯一怀念的是类型安全的第三方 API 接口。当然,你可以根据 OpenAPI/Swagger 规范生成 OpenAPI 客户端,但你仍然要听从第三方 API 提供商的摆布,而且他们有规范这一事实并不一定意味着他们会遵守规范,不会破坏他们的 API,所以我不确定我们能否解决这个问题。

一旦运行,非常稳定

我从未遇到过 Rust 进程崩溃的情况。我遇到过一次 Node 进程崩溃。除非你在代码中使用 .unwrap()(基本上就是说 “如果结果是错误,就崩溃”),否则你的进程很有可能永远不会崩溃。我的代码中有一些 .unwrap() 调用,主要是在初始化过程中,所以如果配置文件丢失或环境变量未定义,我除了让进程崩溃外什么也做不了。但总的来说,Rust 要求你显式地处理错误。所有返回 Result<Something, Error> 的结果都要求你通过 ? 运算符将错误冒泡出来,或者对其执行匹配语句。在大多数情况下,这样做是合理的。但在某些情况下,这有点令人讨厌(例如,当你知道从 i64 到 i32 的转换会成功,因为它保存的数字完全在 i32 的边界内时,你仍然需要执行 try_from(),它返回一个结果)。不过,即使遇到恼人的情况,我也不会使用 .unwrap(),而是会记录警告并返回一些合理的默认值。编程是不可预测的,所以至少这样我的程序可以继续运行,同时记录下我可能犯错的地方。

令人难以置信的工具

JustFax 不仅仅是 Rust,它还有用 TypeScript 和其他 JS 框架编写的内部系统。我发誓,每次创建一个新的 TypeScript 项目,都会有一些变化。要么是某个工具的配置文件变了(看着你 eslint);要么是 typescript 有了新的模板;要么是 express 不再酷了,现在大家都用 fastify;要么是 express 又酷了;要么是 ts-node 不好用了,现在我们用 tsx;要么是这个模块只有 esm,而这个模块是 cjs,所以你自己想吧。在 TypeScript 中总会有这样那样的问题。你工作的方式,你检查代码的方式,vanilla JS 工作空间的方式与 TypeScript 相比都是如此。

但使用 Rust 就不一样了。Cargo init – boom,你会得到一个新项目。想要工作区?没问题,完美运行。clippy帮你搞定。它只是减少了许多模板和心理疲劳。

编译时间仍然是个问题

毫无疑问,Rust 最大的缺点就是编译时间。尤其是当你使用 sqlx 或 maud 等严重依赖宏的工具时,开发过程中的 LSP 和编译器都会开始挣扎。随着项目的发展,我收集了更多的第三方依赖项。虽然我尝试将多个软件包之间的共同依赖关系放在 Cargo.toml 根目录下进行优化,并积极地只选择所需的依赖特征,但在编译时间上仍有困难。我提到过在 CI/CD 中初始编译大约需要 6 分钟,而现在大约需要 20 分钟。根据网上不同的博客,优化编译是有可能的,但这需要类似外科手术的过程,在多分期的 docker 容器中积极缓存依赖关系。我现在没时间做这个,需要把更多精力放在业务方面,但我稍后会做的。

Mac M2 上的本地编译是可以管理的,尤其是在增量编译时,但这需要付出存储空间的代价。我偶尔会运行一次 cargo clean,这样就会删除几十 GB 的缓存。不过,即使是增量编译,也远比不上 JS 世界中的即时热重载。因此,回到我的第一点,“修改一小段代码,立即测试 ”的开发周期就会被打破。这就是 JustFax 的一些内部和外部工具仍然使用 TypeScript 编写的原因。Rust 有点像 “编写大量代码、编译、检查 ”的流程,而不是 Node/JavaScript 那种 “修改一行,alt-tab 浏览器 ”的工作流程。我对此基本没意见。我得到了很多回报,比如真正的类型安全。我相信,CI 管道是可以修复的,只是我需要更多时间来解决它。

不同的工具

Rust 有擅长的地方,比如纯粹的后台。像 axum 这样的框架提供了构建 API 服务器所需的大量组件,而且大多数(流行的)外部 API 都有一个 Rust API 客户端。当然,构建这些 API 的公司通常不会对它们进行维护,但至少我们可以使用它们。不过,网络上的大多数示例都不包括 Rust。因此,你只能自己摸索如何与这个或那个 API 集成。

这也是 Rust 开发中经常出现的问题,至少在网络方面是这样,其他类型的应用我就无法评论了。我经常发现自己在阅读源代码,或在 GitHub 上查找与我遇到的问题类似的问题。LLM 很少能提供正确的解决方案,因为大多数软件包都比较小众。Rust 社区对其软件包的文档和示例非常重视,这一点很棒,但不可避免的是,你最终会遇到一些既没有文档也没有示例的边缘情况,这时候你很可能不得不阅读源代码来解决问题。

而且有些应用并不适合 Rust,尤其是当你来自快速原型开发环境时。我还是更喜欢使用 Astro 或 Svelte 在 TypeScript 中编写前端。Rust DX 在这方面并不出色。由于每次变量变化都需要重新编译代码,这使得前端开发的迭代速度太慢。

总之,我很高兴一年前选择了 Rust。它不仅帮助我获得了我非常喜欢的 $MAIN_JOB,还帮助我构建了更好的软件。我期待着开发 Rust 的第二年。

本文文字及图片出自 One year of Rust in production

你也许感兴趣的:

共有 4 条讨论

  1. 传真号码列表在确保可靠通信方面继续发挥着重要作用。通过维护结构良好的列表,企业可以促进更顺畅的互动,坚持遵守法规,并确保重要文件及时送达预期收件人。

  2. You can purchase a fax number directory service from a reputable data provider or even create your own list by collecting fax numbers from business cards, websites, or other sources. It is important to ensure that the list of fax numbers you use is accurate, up-to-date and complies with data protection laws.

  3. B2B 清單受到高度讚賞。 B2B清單在當今的數位人群中發揮了非常重要的作用。它與其他品牌有很大不同。該品牌在全球範圍內獲得了知名度和認可。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注