(rust语言和go语言)(rust语言适合开发什么)

原译文为繁体【譯】Rust vs. Go https://jmmv.dev/2018/07/rust-vs-go.html

本文译自 Julio Merino 的 Rust vs. Go。 Julio Merino 目前在 Google 任职,在 Google 工作超过 8 年,无论工作内外,都接触开发不少 Go 语言,并撰写 Rust 点评系列文,来听听他对 Rust 与 Go 的想法吧。

Thanks Julio Merino for this awesome article!

欢迎来到「Rust 点评」系列特别篇,也是我在系列文开始就承诺撰写的主题,将探讨一个难以忽视的大哉问:Rust 与 Go 孰优孰劣?

这么比较并没有根据,所以不会有标准答案。我认为人们会把这两种语言作伙比较只因为它们几乎同时发布,而且 Rust 的发布像是在回应 Go。除此之外,两种语言都被认为聚焦在系统软件上(system software),但其实它们大相径庭,就算都专注系统软件,各自目标的软件类型也不尽相同。

Go 可以视为「做对了的 C」或是「Python 的替代品」。 Go 在开发网路伺服器与自动化工具的领域发光发热。 Rust 专注在正确与安全性,定位在 C++ 与 Haskell 之间,如同之前提及,可以视为「务实的 Haskell」。尽管 Rust 的语言抽象程度很高,它仍承诺这些抽象是零成本(zero-cost abstraction),也就是说,它应该擅长写任何系统项目。

这篇个人点评基于我用两种语言写了相同的项目 sandboxfs。最初实现是用 Go,而我开发了另一个用 Rust 的实验性改写(还没有完全检验),两个实现都通过相同的测试套件(test suite)。除了透过这次改写来学习语言,也因为当我分析 Go 实现版本的效能时,发现热点总是在 Go 的执行环境(runtime),我想要尝试看看简单的 Rust 改写后效能能否长进,而情况似乎就是如此。随着这次改写,我很讶异原本的 Go 实现版本有不少潜在的并行(concurrency)漏洞,因为许多 Rust 并不允许我利用相同的设计改写。

在开始之前,也许你会想读读两年前我对 Go 语言的点评。读完了吗?我们开始吧!

内存管理

要说 Rust 与 Go 之间最明显的差异,首推各自的内存管理方式。

Go 根据 object 的生命周期,决定 object 要配置在 stack 或 heap 上,而后者会以垃圾回收机制(garbage collection)管理。 Rust 则是手动(explicit)配置在 stack 和 heap 上,而后者会透过 scopes(C++ 用语中称为 RAII)与 ownership/move semantic 管理。

在这个领域中,典型的权衡是垃圾回收与手动内存管理(explicit memory management)。这代表 Go 因为垃圾回收机制带来额外开销,而 Rust 并不会。但对大多数软件而言不会是个问题。试想你上一次写效能需求高,不能有延迟,且 CPU-bound 的应用程序是什么时候了。

若要高效使用 Rust,需要了解电脑内存如何运作。 Go 可以赦免你不懂内存管理,但如同我下一节将提到的,不懂内存并非好事:了解内存运作是成为程序员之路的重要一步。

获胜者:无法决定。 Go 与 Rust 两者都提供更不容易内存泄漏(memory leak)且具有优良人因设计(ergonomic)的内存管理机制。

难度

Go 是一个简单中不失自身优雅的语言。 Go 极度容易几刻钟内上手并搞出一堆代码。然而,如同我们在「Rust 点评:学习曲线」一文(learning curve post)所述,这可能是谬误:在你刚学了任何语言后马上写的代码都不会最符合该语言特性,最佳实践的代码需要花更多时间熬出。

对 Rust 来说,不能否认它是一个复杂的语言,也许没有 C++ 这么复杂,但仍有大量的概念在其中。撰写 Rust 代码的确需要更多精力。对应的收益是,当你的代码写完且成功编译,很大的机率它会安然无恙的运作。这情境无法完全套用在 Go 上,我见过太多太多运行时(runtime)错误崩溃了。

你要选择自己的战争:快速地撰写 Go,再花费额外的时间写琐细的测试与修正 runtime 的问题;或是花时间一步步撰写稳健的 Rust 代码,避免构建后的问题。

获胜者:难以决定。我们可以很简单地说 Go 赢了,但我并不想要这样定论,因为我个人喜欢花更多时间打造一个经得起时间考验的代码,而非在未来还必须追踪复杂的内存与线程问题。

泛型

Go 不支持泛型。 Go 的作者们并没有坚决反对泛型,但他们声称无法简洁地实现或支持,而开始动手做之前,必须先找到完美解法。因此,人们滥用 interface {},将之作为泛型使用,其他 Go 代码也无法受惠于以此实现的原生函数(如 append)。

Rust 刚好具有如你所愿的泛型。这个泛型支持普通型别及 traits,而且你还可以透过 impl 与 dyn 这两个新特性(从 2018 开始),控制 Rust 从 traits 上面产生的泛型机器码(machine code)。

获胜者:不用多说了,非 Rust 莫属。

代码完备性(Code sanity)

我最不能接受 Go 语言缺失的功能是没有提供零抽象成本的方法,透过代码自身来编撰稳健的代码。没错,自动代码格式化和强力建议代码如何写这两件事很美好,但这不足以强制改善代码逻辑。我发现自己常常撰写过长的注解来解释一些完全不可能的条件,或为什么特定参数可以运作,还有变数与互斥锁之间有什么关系⋯⋯,这些都是未来代码会崩溃的病征:注解无法透过编译器检验完备性,而且注解一定会过时。只要有机会,所有东西都该以代码表达。

那么,什么我在 Rust 发现什么 Go 缺少的功能,可令代码更稳健更能防范未然?

断言(Assertions)。断言之所以无价,是因为它让程序员们得以沟通不一定明显的意图。 Rust 有断言,反观 Go 没有。呃⋯⋯Go 的确有 panic 可以来模拟断言,但这作法不被认可,你应该永不使用它。

为什么 Go 没有断言呢?以下是我瞎猜的:因为代码设计师常常误用断言来验证使用者的输入,这样导致有不对或恶意输入时产生 runtime 错误。于是,崇尚务实主义的 Go 要求把所有问题当成可以被控制的错误,从而避免「误用断言」这种情况。 (嗯⋯⋯,这我过些时间想再写一篇独立的文章来讨论⋯⋯)

顺便提个我常常听到的说法:你可以从写了多少断言来判断一个程序员的经验是否老到,而越多代表越老练。欢迎对这个想法有任何评论。

标注(Annotations)。有时候该用到的输入参数没使用到,或该检查的回传值没检查。或其他时候,你知道一个特定函数永远不会返回,而且希望给予呼叫端这项资讯,让编译器闭嘴。举个例子,缺少 return 陈述句。 Go 没有这些标注,让程序员很难清楚表达意图。而 Rust 嘟嘟好有这些标注。

更糟的是,Go 有些特定内部函数的行为如 panic,这些编译器知道无法回传,但不可能在你手写的 Go 代码中表达这件事。如果考虑这问题和前面提及泛型是同个缺点,再次提及比较有点失允。

注释即文件(Docstrings)。是的,Go 有 docstrings,但特别基础。虽然大部分可以正常运作,但写了一堆 Java 之后,知道事先定义好结构的 docstrings 能提供不少价值。许多工具可协助检验文件的完备性。举例来说,IntelliJ 可以验证参数名称是否对应真正的函数参数,并且交叉参照其他类别是否也合法。

Rust 的 docstrings 支持 Markdown 而比 Go 来得好些,但仍无明确的撰写指引:似乎没有一种标准方式撰写,也不支持替每一项目个别撰写文件,工具更无法交叉检查 docstrings 是否和代码吻合。

错误检查(Error checking)。我是少数(?)喜爱 Go 手动错误传递的一分子。你说得没错,写一堆错误检查很恼人,但这样做迫使你使用与其他语言不同的方式思考错误这档事。

很不幸,Go 选择的写法有些问题:一个函数总是回传一个结果值与一个错误,呼叫端可以决定先检查错误,再检查值。语言本身并无强制做这件事,而我看多太多太多错误是在检查错误前先取值所导致。另一方面,Rust 带着更高端的型别封装了值或错误,加上没有 null 这个特点,这代表了呼叫端永远不会在错误存在时取得结果值,反之亦然。可以参考看看 Result 型别还有我写有关 match 关键字的文章。

让我下个结论,一个正向的声明,这两个语言都不提供自动型别。举例来说,Rust 与 Go 强制程序员们整数转型至不同大小,于是任何可能溢位(overflow/underflow)之处显露无遗。提醒你一下:Go 在这方面比 Rust 稍强些,因为 Go 的型别别名(type alias)语义上被视为不同的型别,编译时需要手动转型,而 Rust 则是当作语法上的别名(就像 C typedef 所作所为)。

获胜者:Rust 轻松胜出。你可以主张这只是对 Go 缺乏功能的一些抱怨,而且应该接受 Go 就是这种风格,但我不行:这些抱怨就是讨论上述失能让 Go 无法写出防范未然,晶莹剔透的代码。

效能分析

Go 的简便无所不在,连让代码最佳化的工具也不例外。从效能分析(profiling)来看,Go 内建追踪 CPU 和内存用量的机制,且能与 pprof 工具整合。可以很容易检查 Go 代码并取得有用的资料来最佳化。

我还没发现任何 Rust 的分析工具可以和 Go 的工具一样整合 pprof。当然,有一个函数库可以生成类似 pprof 的追踪资料,但我无法简易上手,安装上也有点诡异(需要 gperftools 以显示在系统上)。这篇旧文有相关资讯和工具可供参考。

获胜者:就我目前所知,Go 在这方面大胜。

构建速度

打从娘胎开始,Go 就被设计为尽可能快速构建。就我所知,这是为了减少 Google 内部大型应用程序构建时间而做的尝试。我猜测这合理解释为什么 Go 选择 duck typing 来避免组件间强耦合,进而加速增量编译(incremental compilation)。当我第一次在 NetBSD 自我建立(bootstrap)Go 整个工具链时,我非常震惊,整个流程只需要几分钟。我以前使用 clang 需要几小时建立环境。

Rust 是家喻户晓的编译缓慢,所有完备性检验(sanity check),例如 borrow checker 和泛型,并不是白吃的午餐。我听说有机会可以改善,但第一,我还没研究过;第二,这说法尚未实现。

获胜者:简单啦,Go。

构建系统

依照现代编程语言的惯例,Go 和 Rust 都有自己的套件管理与套件相依追踪工具。

在 Go 语言,有穷极简单的 Go 内建工具允许我们取得套件与它的相依套件,且无需任何设定档就可构建整个项目。听起来非常诱人,但事实上略违反基本工程惯例,有点危险。举例来说,Go 的工具总是从网站如 GitHub 取得最新快照版本的相依套件,但完全没有任何机制指定版本,也无法确保恶意代码混入。我认为这应是受 Google 超大单一储存库(monorepo)如何运作,以及「在最新版构建」(build at head)的哲学影响,但这不太符合开源社群生态的期待。显然 Go 社群最后接受了他们需要一个更好的解决方案,有许多提案也尝试改善这种状况。

在 Rust 这边,我们有 Cargo,在项目中使用 Cargo 会需要比 Go 内建机制多一点点设定,但就只有一点点:典型的 Cargo.toml 只需列出几行相依套件和该项目基本资料(metadata)即可。 Rust 社群使用 Cargo 可以解析的语意化版号管理相依。换言之,Rust 和 Cargo 设计上就支持他们身处的生态系,而非赶鸭子上架。 Cargo 完全是 Rust 最美好的功能之一。

有趣的是,人们可以透过 Bazel 构建 Go 与 Rust 的代码。这种情况下,Bazel 透过一系列条件(rules_go)修正了上述提及 Go 的已知问题:这些条件允许相依套件以及工具链固定版号。至于 Bazel 对 Rust 的支持还很阳春,对应的条件(rules_rust)并没有太多功能。

获胜者:只考虑原生构建系统,Rust 的 Cargo 工具链胜出。如果把 Bazel 拉进来讨论,目前是 Go 略胜一筹。

单元测试

对任何代码来说,自动化测试是确保代码依照期望执行,并保证代码演化时仍能正确执行(例如:没有写测试就无法做大规模的重构)。 Go 这方面的表现令我嗤之以鼻,我想我会保留内容给另一篇专文讨论。 Go 的 testing 套件看来完全不鸟现代的测试技术,一心走自己认定的套路。那欸安捏?舍弃断言(assertion)不用,导致只有测试函数自己可以使测试失败,不允许其他辅助函数或固定资料(fixture)。对不复杂的单元测试来说挺诱人的,但往往不好控制。最终,更复杂的测试让真正的待测逻辑变得更隐晦,更难以理解。

谢天谢地,Go 有个第三方函数库 testify 提供类 JUnit 的测试框架。这函数库使用合理的语义进行测试。较严重的问题是 Go 社群文化较武断,你可能没有机会在项目中使用它。

另一头,Rust 的测试函数库较贴近你预期的其他语言的行为。换言之,你有断言可以用。我发现比较奇怪的点是,Rust 推荐你把测试写在与待测原始码同一份档案中。由于我还没写足够的测试,不清楚在真实世界中这种做法到底会如何?

获胜者:我很想说是 Rust,因为 Go 的测试途径太令我失望了,但 Go 有 testify 套件存在,我必须说两者平分秋色。

结论

每一种语言皆有优缺点,这些小小的权衡抉择是我藉由 Go 与 Rust 做比较来 证明这个概念。在每个部分胜出的语言都不一样,有些时候你根本无法决定谁更有优势。

您身为工程师的职责就是了解每个项目该如何权衡,并替项目选择最佳的工具。 Rust 与 Go 只是工具,挑一个最适配团队,项目,还有你自己吧(按照这个顺序)。

如果你希望我能更具体,我会强烈推荐在座的各位学习 Rust,与 borrow checker 与 ownership 和平共处。即使最终你并没有使用 Rust,过程中所学都会回馈到其他语言上,连 Go 也不例外。

声明:我要去上班所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,版权归原作者WASM爱好者所有,原文出处。若您的权利被侵害,请联系删除。

本文标题:(rust语言和go语言)(rust语言适合开发什么)
本文链接:https://www.51qsb.cn/article/m8odj.html

(0)
打赏微信扫一扫微信扫一扫QQ扫一扫QQ扫一扫
上一篇2023-01-20
下一篇2023-01-20

你可能还想知道

发表回复

登录后才能评论