rust语言 go语言比较

Z, ZLW 1260

rust语言 go语言比较从以下几个方面:1、性能和工作效率;2、并发能力;3、内存管理;4、难度;5、泛型;6、代码完备性(Code sanity);7、构建速度;8、构建系统;9、单元测试。从Rust语言与Go语言比较来看,两者各有各的优势,具体如何选择可以根据实际项目来定。

1、性能和工作效率

Rust语言和Go语言的运行速度几乎与C++/C相当。当人们进行编码工作时,Go语言的开发速度比Rust语言要快。而在软件开发的多个步骤中,与Rust语言相比,Go语言的性能会有所下降。

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

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

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

2、并发能力

Go语言的语法内置并发性,目前Rust语言并没有,所以就并发能力而言,Go语言更占优势。

3、内存管理

通过零成本抽象,Rust使用编译策略进行内存管理。如果Rust程序中存在任何安全问题,它将无法通过编译阶段。和Rust一样,Go也是内存安全的。但是对于Go语言是在运行时自动处理的,有时也会引起问题。因此就内存管理而言,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 的应用程序是什么时候了。

4、难度

Go 是一个简单中不失自身优雅的语言。 Go 极度容易几刻钟内上手并搞出一堆代码。

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

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

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

5、泛型

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

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

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

6、代码完备性(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 无法写出防范未然,晶莹剔透的代码。

7、构建速度

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

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

获胜者:简单啦,Go。

8、构建系统

依照现代编程语言的惯例,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 略胜一筹。

9、单元测试

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

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

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

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

回复

我来回复
  • 暂无回复内容

站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部