2023 年,我们有一千个学习 Rust 的理由。
8 月 7 日,Rust 基金会发布了 2022 年度 Rust 调查报告结果,报告显示 Rust 采用率不断提高,超过 90% 的调查受访者表示自己是 Rust 用户;29.7% 的受访者表示,他们在工作中的大部分编码工作都使用 Rust,比上一年显着增加了 51.8%。
毋庸置疑,Rust 以其卓越的内存安全性和并发性能正日益成为开发者关注的焦点。然而,同样令人难以忽视的是 Go,这门曾被评选为年度编程语言的相对比较“老牌”的选手。
Go 语言诞生于 2009 年,一开始就因其独特的并发模型和强大的性能优势而受到了极大关注。值得注意的是,跟 Rust 语言一样,Go 语言的创建者也同样“讨厌”C++,并且 Go 同样也都是云原生的主导语言。
而在 Stack Overflow 2022 开发者调查中,对于“让人爱恨交织的编程语言”这个问题,在 7 万份回复中,程序员们明显也更为偏爱 Rust,86% 的人表示喜欢 Rust,而 64% 的人表示喜欢 Go。面对 Rust 的火爆现状,一些开发者发出了灵魂提问:2023 年,Go 还值得学习吗?
另外,这两天,到底是该选 Rust 还是选 Go,也成为了 Hacker News 上的一个热门话题:
一位挺 Rust 的网友说道:“我也为这个选择烦恼了很久。最终 Rust 胜出了。首先,我感觉 Rust 更接近于以前 Pascal 时代的东西,你可以控制一切;其次,如果 wasm 和相关技术大爆发,Rust 将是一个更安全的选择;然后,我们已经有了 Python 用于快速开发,因此选择一些更极端的东西是有道理的,Go 在某种程度上处于中间地带。最后,Rust 应用于内核且备受关注,所以不太可能会被淘汰。”
另一位持反对意见的开发者则表示,“我从事 Go 开发已经快十年了,但最近我也尝试了下 Rust。我认为目前有一些对 Rust 的强制性和误导性偏好,从我在各种初创公司的经验,包括我目前所在的公司来看,对于后端开发来说,Go 是迄今为止最佳选择!注意,在性能、功能或其他方面……这两种语言非常非常相似!”
不得不说的是,Go 和 Rust 绝对都是优秀的编程语言。它们现代、强大、应用广泛,而且有着卓越的性能表现。但如果直接对比 Go 和 Rust 谁更好之类的,真的没啥意义,因为每种编程语言都代表着背后一系列深层次的权衡。不同的语言会针对不同的需求进行优化,因此我们在选择语言时,也应该考虑自己想要用它解决什么样的问题。所以我们将从 Go 和 Rust 语言的适用场景出发,探讨下 Go 与 Rust 的设计之“道”。
虽然 Rust 和 Go 在语法和风格上差别很大,但它们都是构建软件的一流工具。下面咱们开始具体分析。
Rust 和 Go 有很多共同点,所以人们才经常把二者拿来相提并论。那它们有哪些共同目标?
Rust 是一种低级静态类型的多范式编程语言,更多关注安全性和性能。
—Gints Dreimanis
而:
Go 是一种开源编程语言,能够轻松构建起简单、可靠且高效的软件。
—golang.org
Go 和 Rust 都属于重视内存安全的现代编程语言。在 C 和 C++ 等旧语言发展的这几十年间,我们已经清楚地意识到,引发错误和 bug 的核心原因之一,就是对内存的不安全 / 不正确访问。
于是 Rust 和 Go 各自给出了不同的解决思路,但二者的目标都是在内存管理方面更智能、更安全,帮助开发者编写出正确且性能极佳的程序。
二者都属于编译语言,也就是说可以将程序直接翻译成可执行的机器代码,这样就能把程序部署成单一二进制文件。跟 Python 和 Ruby 等解释性语言不同,我们不需要随程序一同发布解释器和大量的库 / 依赖项。作为这个核心优势的直接体现,Rust 和 Go 程序的运行速度往往比解释性语言更快。
Rust 和 Go 都属于功能强大且可扩展的通用编程语言,大家可以用它们开发出各种现代软件——从 Web 应用程序到分布式微服务,还包括嵌入式微控制器和移动应用等等。
两者都拥有优秀的标准库和蓬勃发展的第三方生态系统,外加强大的商业支持与庞大的用户基础。二者已经存在多年,并将在未来几年继续保持旺盛的发展势头。如今,学习 Go 或者 Rust 将是非常合理的时间和精力投入方向。
两者既不过多偏向函数式语言(例如 Scala 或 Elixir),也不完全面向对象(例如 Java 和 C#)。相反,虽然 Go 和 Rust 都具备函数式及面向对象编程的功能,但却始终强调务实取向——即以最合适的方式解决问题,而不是通过“意识形态”强迫大家用特定的方法做事。
但如果您确实喜欢函数式编程风格,那 Rust 这边的相关工具选项更多,这也是 Rust 优于 Go 的一点。
我们当然可以争论什么才是真正“面向对象”的语言。但公平地讲,C++、Java 或者 C# 用户所期望的那种面向对象编程风格,在 Go 或者 Rust 中确实不存在。 —Jack Mott
Rust 和 Go 都为大规模编程提供不少有用功能,所以它们都能适应大开发团队作战和大体量代码库的现实需求。
例如,C 程序员多年来一直在争论应该把括号放在哪里,还有代码要不要用制表符或空格进行缩进;但 Rust 和 Go 早已使用标准格式化工具(Go 有 gofmt,Rust 则是 rustfmt)彻底解决了这些问题。它们会使用符合规范的风格自动重写你的代码。
并不是说这种特定的格式有多精妙,而是 Rust 和 Go 程序员更加务实、宁愿选择统一的执行标准。
gofmt 的风格也许不是每个人的最爱,但 gofmt 却能帮到每一个人。 —Rob Pike
这两种语言的另一大优势,体现在构建管线上。二者都有优秀、内置且性能出色的标准构建与依赖项管理工具。就是说程序员不必跟复杂的第三方构建系统对抗,也用不着每隔几年就学习一种新系统。
我在职业生涯早期用的是 Java 和 Ruby,所以编写 Go 和 Rust 代码一直让我有点畏惧、觉得自己掌握不了。但等到进入谷歌并看到用 Go 编写的服务时,我才真正松了口气,因为我发现它很容易构建和运行。 Rust 也是如此。虽然我只在小规模项目上进行过研究,但也看得出它的易用性。我希望那些能够无限配置的构建系统早点成为历史,现在的新语言都附带自己的专用构建工具而且能够开箱即用,这样不好吗? —Sam Rose
聊了这么多问题,再加上两种语言都设计得如此精良、功能如此强大,那这场比拼到底有没有结果?或者说,既然二者都是非常出色的选项,那为什么人们还会在社交媒体上出离愤怒,撰写长篇累牍的评论博文放出“白痴才用 Rust”或者“Go 根本不能算编程语言”之类的狠话?
有些人当然只是为了宣泄情绪,但这显然无助于解决实际问题。至少在项目中该用哪种语言、或者该靠哪种语言闯荡编程世界这种事上,嗓门大显然无助于做出正确选择。
下面咱们回到成年人的讨论,看看理性分析之下 Rust 和 Go 之间如何互有长短。
之前已经提到,Go 和 Rust 生成的程序运行速度都很快,因为它们会被编译成本机机器码,无需通过解释器或虚拟机这个步骤。
但 Rust 的性能还是要更胜一筹,甚至能够与被称为业界性能标杆的 C 和 C++ 相媲美。而且跟这些老牌语言不同的是,Rust 还提供内存安全与并发安全机制,同时几乎不影响执行速度。Rust 还允许开发者构建复杂抽象,又无需在运行时承受性能损失。
相比之下,虽然 Go 程序的性能也不错,但其设计重心主要在于开发速度(包括编译)、而非执行程度。Go 程序员更倾向于代码的清晰可读,所以运行速度要稍逊几分。
Go 编译器也不会花费太多时间来生成最高效的机器码,它更关心如何快速编译大量代码。所以在运行时基准测试中,往往是 Rust 程序要压 Go 程序一头。
Rust 的运行时性能还具有良好的一致性和可预测性,因为它没有使用垃圾收集。Go 的垃圾收集器非常高效,而且做了优化以尽可能缩短暂停时长(随着 Go 新版本的发布,暂停时长也是越来越短)。但无论如何,垃圾收集总会给程序的行为方式带来一些不可预测性,而这对某些特定应用(比如嵌入式系统)而言可能很严重、甚至完全不可接受。
因为 Rust 的目标是让程序员完全控制底层硬件,所以 Rust 程序都能深度优化以接近机器的最大理论性能。如此一来,Rust 就在执行速度胜过其他一切的特定应用场景下成为最佳选项,此类用例包括游戏编程、操作系统内核、网络浏览器组件和实时控制系统等。
如果一种编程语言过于难学、把大多数人都挡在了门外,那它的性能再强也没有意义。Go 在设计上似乎就是刻意要跟 C++ 等复杂度不断提升的语言区分开来:它语法极少,关键字也极少,就连功能都不多。
这意味着 Go 语言很容易上手,稍微了解之后就能用它编写出各种程序。
Go 确实非常容易学习。之前就经常听人提到这一点,但实际使用后我仍惊讶于它竟能快速提高工作效率。感谢 Go 语言、相关文档和工具,我只用了短短两天就编写出了有趣且可以提交的代码。 —Rust 程序员对于 Go 语言的早期印象
这里的重点就是“简单性”三个字。当然,简单并不代表容易。可一门小而简单的语言,学起来肯定要比大而复杂的语言要轻松。实现一种效果的方法并不多,所以高质量的 Go 代码看起来几乎都是一个样。这还带来另一个好处:我们可以快速理解某项自己不熟悉的服务到底在做什么。
fmt.Println("Gopher's Diner Breakfast Menu")
for dish, price := range menu {
fmt.Println(dish, price)
}
Go 的核心本体虽然很小,但标准库却非常强大。也就是说,除了 Go 语法之外,我们的学习曲线还必须考虑到标准库这个部分。
另一方面,把功能从语言转移到标准库,意味着大家只需要专注学习跟当前开发需求相关的库。Go 在设计上也充分考虑到大规模软件开发需求,能够有力支持大型代码库和开发团队。在这类场景下,新加入的开发者必须能够快速上手。为此,Go 社区一直将程序的简单、明确、通用和直接放在首位。
使用 Go,我们可以快速完成工作。Go 是我用过的最高效的语言之一,它的座右铭就是:马上解决实际问题。 — Matthias Endler
Rust 比其他几种编程语言支持更多复杂性,所以对应的实现范畴也更大些。 — Devathon
Rust 经过专门设计,包含多种强大且有用的功能,可以帮助程序员用最少的代码完成更多任务。例如,Rust 的匹配功能就可快速编写出灵活且富有表达力的逻辑:
fn is_prime(n: u64) -> bool {
match n {
0...1 => false,
_ => !(2..n).any(|d| n % d == 0),
}
}
但也因为 Rust 在设计上考虑得多,所以学起来也就更困难一点,特别是在起步阶段。但没关系,毕竟 C++ 或者 Java 也有很多东西要学,甚至还无法提供内存安全之类的 Rust 高级功能。所以批评 Rust 太过复杂的声音实在没啥道理:它在设计上就是在强调表达能力和丰富的功能,我们不可能在占了好处的同时又指望着它能那么简单纯粹。
所以 Rust 当然有自己的学习曲线。但只要跨过了这道难关,后面就是一马平川了。
如果您已经准备好学习更复杂的语法和语义(以及更高的代码可读性门槛),并以此换取最高水平的性能表现,那 Rust 甚至足以跟 C++ 和 D 分庭抗礼。 — Dave Cheney
Rust 和 Go 之间虽然彼此借鉴了一些功能(比如说泛型),但公平地讲,Rust 的功能还是更胜一筹,Go 的功能相对要匮乏一点。
大多数语言都为并发编程(即同时执行多项操作)提供某种形式的支持,但 Go 则是从头开始就为此而设计。Go 不使用操作系统线程,而是提供一种轻量化的替代方案:goroutines。每个 goroutine 都是个独立执行的 Go 函数,Go 调度程序会将其映射至控制下的操作系统线程之一。也就是说,调度程序可以非常高效地管理大量并发 goroutine,且只须使用有限数量的操作系统线程。
因此,我们可以在单个程序中运行数百万个并发 goroutine,又不必担心引发严重的性能问题。正因为如此,Go 成为 Web 服务器和微服务等大规模并发应用场景下的完全解决方案。
Go 还为 goroutines 提供 channels,这是一种快速、安全、高效实现数据通信和共享的方法。Go 的并发设计水平确实很高,使用体验也相当轻松愉快。
一般来说,并发程序的设计难度很大,在任何语言中构建起可靠且正确的并发程序都绝非易事。但由于在立项之初就考虑到这方面需求,所以 Go 中的并发编程机制已经做得尽可能简单且得到良好整合。
Go 让我们能更轻松地构建起一个能精心解构的应用程序,这样的应用程序可以作为一组微服务进行部署,并充分发挥并发性优势。Rust 也不是做不到,只是实现起来更难一些。从某种意义上讲,Rust 更适合那些绝不允许因内存问题而引发安全漏洞的程序员;但相应的,他们在执行某些对其他语言(包括 GO)来说较为简单的任务时,就得付出更多心力。 —Sonya Koptyev
相比之下,Rust 中的并发机制刚刚落地、还没有最终稳定,所以欢迎大家继续关注这个活跃的开发方向。这样也有好处,比如 Rust 的 rayon 库就提供一种非常优雅且轻量级的方法,能够将顺序计算转换为并行计算。
能有用于生成 goroutine 和使用 channels 的轻量级语法真的太棒了。这就是语法之力的直接体现,种种小细节也让 Go 的并发编程体验比其他语言好出一大截。 —Rust 程序员对 Go 的早期印象
虽然在 Rust 中实现并发程序可能不太容易,但仍然完全可行,而且这些程序还能获得 Rust 精心设计的内存安全保障。
以标准库的 Mutex 类为例:在 Go 当中,我们可能会在访问某些内容前忘记获取互斥锁;但在 Rust 这边则完全不需要担心。
Go 专注于把并发作为最核心的概念之一。这倒不是说我们就没法在 Rust 中实现跟 Go 类似的并发性效果,只是实现难度对于程序员多少是种考验。 —Dave Cheney
前文已经提到,Go 和 Rust 都会以各自的方式防止各种常见的编程错误,特别是跟内存管理相关的问题。但 Rust 走得更远,可以说是不遗余力地保证大家不致搞出意料之外的安全纰漏。
Rust 的编译器简直是严格到迂腐,它会检查我们使用的每个变量、引用的每个内存地址。它避免了潜在的数据竞争情况,还会通知你存在未定义行为。在 Rust 的世界中,并发和内存安全问题几乎不可能出现。 —为什么选择 Rust?
也就是说,Rust 的编程体验跟几乎所有其他语言都有所不同,而且在刚刚接触时可能相当具有挑战。但在不少开发者看来,这份付出显然物有所值。
对我来说,Rust 最大的优势就是编译器成了我的好助手,它不会放过任何检测得到的 bug(说真的,有时候我感觉它就像会魔法)。 —Grzegorz Nosek
包括 Go 在内,很多语言也提供帮助程序员避免错误的工具,但 Rust 把这种效果提升到了新的水平。很多不正确的程序甚至根本没办法编译。
在 Rust 中,各种库工具都能帮助程序员防止用户犯错。Rust 允许我们指定一段数据,然后保证它不归属于任何其他事物、也不会被任何其他事物所篡改。我想不起以往还有哪种语言会提供这么多防止意外误用的工具,这种感觉堪称美妙。 — Sam Rose
“与借用检查器作斗争”是 Rust 新人们必须要过的一关,但在大多数情况下,检查器并不是真正的敌人。它发现的问题确实是代码中的真实 bug(或者至少是潜在 bug)。它可能迫使我们从根本上重构自己的程序来避免此类问题——如果各位确实把正确性和可靠性当作首要任务,那这种严格要求显然是件好事。
换个角度想,不改变编程方式的新语言,能叫新语言吗?而且在使用其他语言时,Rust 教会我们的安全思维同样意义重大。
如果大家选择了 Rust,往往是因为要使用它提供的保障性设计:关于空指针 / 数据竞争的安全性、可预测的运行时行为,还有对硬件的完全控制。如果这些对你来说毫无意义,那确实没必要非得使用 Rust。毕竟这些好处背后是有代价的:上手很费劲。你得改掉坏习惯并掌握新概念。刚开始的时候,大家都会被借用检查器折磨得死去活来。 — Matthias Endler
上手 Rust 编程模型的实际难度,可能取决于大家之前用过哪些其他语言。Python 或者 Ruby 程序员可能觉得 Rust 限制太多,但其他人可能觉得这种清晰明确的约束也不错。
如果你是一名 C 或者 C++ 程序员,曾经花几个礼拜在语言中查找内存安全 bug,那你一定会爱上 Rust。于是“跟借用检查器作斗争”就变成了“编译器还能这么用?爽!” — Grzegorz Nosek
如今的服务器程序包含着数千万行代码,由成百上千名程序员编写而成,并且几乎每天都在更新。Go 在设计和开发上,充分考虑到了此类环境下的工作效率提升需求。Go 的设计考量因素包括严格的依赖项管理、软件架构随系统增长的适应性,还有跨组件边界的健壮性。 — Rob Pike
当大家独自或者在小团队中解决问题时,要选简单的语言还是丰富的语言纯属个人喜好。但随着软件规模的扩大、复杂度的提升、团队的膨胀,两类语言之间的差异才开始真正显现出来。
对于大型应用程序和分布式系统,代码执行速度的重要性往往低于开发速度:像 Go 这种刻意强调精简设计的语言能够缩短开发新手的适应时间,也让他们能更快参与到大型代码库的贡献当中。
使用 GO 语言,初级开发者往往更容易提高工作效率,但中级开发者则更难引入复杂的抽象并因此导致问题。正因为这种特性,在企业软件开发领域,Rust 的吸引力往往不及 Go。 — Loris Cro
在涉及大规模软件开发时,明确易读总是比精巧优雅更重要。Go 的局限性实际使其比 Rust 等更复杂、更强大的语言,要更适应企业和大型组织的需求。
虽然 Rust 和 Go 都是高人气且得到广泛应用的现代语言,但二者间并不是真正的竞争对手,因为它们所面向的用例可以说完全不同。
Go 的整个编程方法就跟 Rust 完全不同,这些特性一方面特别适合某些人,但另一方面也会彻底激怒某些人。这很正常,因为如果 Rust 和 Go 都在以基本相似的方式解决基本相同的问题,那我们干嘛还需要两种独立的语言?
那么,我们能不能从 Rust 和 Go 采取的方法入手,解读它们各自的本质呢?下面就一起来试试。
“要垃圾收集,还是不要垃圾收集”永远是个没有正确答案的问题。一般来说,垃圾收集和自动内存管理能帮助我们快速、轻松地开发出可靠且高效的程序。所以对某些开发者来说,这些都是必不可少的功能。
但也有人认为,垃圾收集和它带来的性能开销与全局暂停,会导致程序在运行时的行为变得不可预测,同时引入不可接受的延迟。这话当然也有道理。
Go 跟 Rust 这两种语言可以说截然不同。尽管二者都可以被简单描述成系统语言或者 C 的替代品,但它们的目标和应用场景、语言设计风格与功能优先级确实差异巨大。垃圾收集就是一大核心差异因素。Go 中的垃圾收集让语言变得更简单、更小巧也更易于理解。Rust 不设垃圾收集则让它速度极快(这一点特别适合那些不仅要求高吞吐量、更要求低延迟的开发者),同时也实现了 Go 根本不可能做到的一系列功能与编程模式(至少是在不牺牲性能的前提下)。 — PingCAP
计算机编程的发展史,可以说是一段日益复杂的抽象发展历程。它让程序员们既能解决问题,又不用太多关注底层硬件的实际运行方式。
这种设计让程序更易于编写、更具可移植性。但对于其他一些程序来说,访问硬件及精确控制程序的执行方式反而更加重要。
Rust 的目标就是让程序员能“贴近硬件”,夺回更多控制权;而 Go 则抽象掉了架构细节,让程序员更贴近问题。
两种语言各有不同的应用范围。Go 擅长编写微服务和典型的“DevOps”任务,但它并不属于系统编程语言。Rust 在强调并发性、安全性及 / 或性能的任务中更为强大,可学习曲线也确实比 Go 更陡峭。 — Matthias Endler
其实对大多数程序来说,性能的重要性是不及代码可读性的。但如果某些项目确实是以性能为先,那 Rust 中的很多设计权衡,将帮助大家把代码的执行速度一路推向极限。
相比之下,Go 更关心代码简单性,甚至愿意为此牺牲一些运行时性能。但 Go 的构建速度无与伦比,这对大规模代码项目来说往往更加重要。
Rust 的执行速度优于 Go。在基准测试中,Rust 速度确实更快,某些情况下甚至能快出一个数量级。但在选择 Rust 语言之前,请先认清一点:Go 在多数基准测试中也没有落后太多,而且也仍然保持对 Java、C#、JavaScript 和 Python 等语言的性能优势。 如果你需要的是顶级性能,那么在这两种语言中任意选择都可以,速度表现绝不会令人失望。另外,如果你正在构建一款处理高强度负载的 Web 服务,而且要求能够纵向 / 横向灵活扩展,两款语言也都能满足需求。 — Andrew Lader
另一方面,如果不强求程序永不出错,那取舍以会不同。大多数代码都不会考虑到长期使用,但某些程序也确实在生产环境中多年运行。
面对这些现实情况,也许我们有必要投入一点额外的时间,来开发并保证程序能够正确、可靠运行,且未来不致引发沉重的维护负担。
Go 和 Rust 都能帮助大家编写出正确的程序,只是具体方式各有不同:Go 提供出色的内置测试框架,而 Rust 则专注通过借用检查器消除运行时 bug。
我的看法是:对于明天就得发布的代码,用 Go;如果是未来五年内必须能稳定运行的代码,那么选 Rust。 — Grzegorz Nosek
虽然 Go 和 Rust 都足以支撑起严肃的开发项目,但大家最好还是能充分了解它们的各种特性和优势。
总之,别人的想法并不重要:只有你自己能决定哪种编程语言更适合你的团队及项目需求。
如果你想加快开发速度,比如说你有很多不同服务需要编写,或者开发团队本身规模庞大,那么 Go 语言肯定是正确答案。Go 特别关注并发性设计,而且会敏锐地揪出不安全的内存访问行为(Rust 也可以),但又不强迫你逐一管理每处细节。 Go 快速而强大,但它的核心亮点还是帮助开发人员摆脱困境、专注于简单性和统一性。在另一方面,如果你需要竭尽全力发挥每一丝性能空间,那 Rust 才是最理想的选择。 —Andrew Lader
总结希望这篇文章能帮助大家理解 Rust 和 Go 的各自亮点。如果可能的话,各位最好能多少体验一下这两种语言,因为它们在任何技术道路上都非常有用,哪怕对业余编程爱好者也是如此。
但如果您的时间只搞认真钻研一门语言,那请千万先把 Go 和 Rust 各自的专长和倾向性搞清楚,之后再做选择。
当然,关于编程语言的知识只是成就一名成功软件工程师的很少一部分。除了编程之外,工程师们还得精通设计、工程、架构、沟通和协作。只要大家能把后面这几样做好,那无论你选择哪种编程语言,都将成为一名出色的软件工程大牛。