Go vs Rust
我在过去的几周开始使用 Go。这是我第一次在一个较大且严肃的项目中使用 Go。之前我对 Go 有过很多了解,并且在研究 Rust 的特性时,曾经使用例子和玩具程序。然而,真正的编程经验是完全不同的。
我认为写写我对它的印象会很有趣。我会尽量避免与 Rust 进行过多比较,但作为我熟悉的语言,比较难免。我应该提前声明对 Rust 有强烈偏爱,但我会尽量客观。
总体印象
使用 Go 编程很舒适。它拥有我在库中想要的一切,而且没有太多棘手的地方。学习它是一个流畅的体验,它是一种设计良好且实用的语言。例如:一旦你熟悉了语法,许多其他语言的习惯用法也能适用于 Go。学会了一些 Go 后,相对容易预测其他功能。有些其他语言知识,我能够阅读 Go 代码并在不进行太多搜索的情况下理解它。
与使用 C/C++、Java、Python 等语言相比,这种方式更少令人沮丧且更高效。然而,它确实感觉像是那一代语言的一部分。它从中吸取了一些教训,我认为它可能是那一代中最好的语言,但它绝对是那一代的一部分。它是一个渐进改进而不是一种完全不同的东西。
明确一下,这不是一种价值判断;在软件工程中,渐进通常是好事。一个很好的例子就是 nil
:像 Rust 和 Swift 这样的语言已经移除了 null
的概念,消除了一整类错误。Go 使得它不那么危险:没有空值,它区分了 nil
和 0
。但核心思想仍然存在,空指针解引用的常见运行时错误也依然存在。
学习性
Go 的学习曲线非常平缓。我知道这经常被宣传为一个好处,但我真的很惊讶自己是如何迅速地提高生产力的。多亏了语言、文档和工具,我在只有两天的时间里就能编写有趣且可提交的代码。
影响 Go 学习性的一些因素是:
- Go 是小巧的。很多语言尝试变得小巧,但 Go 确实是小巧的——非常小巧。(这在大多数情况下是一件好事,我对此所需的纪律感印象深刻。)
- 标准库很好(而且同样很小)。从生态系统中找到并使用库非常容易。
- 语言中很少有其他语言没有的特性。Go 从其他已有的语言中汲取了很多经验,进行了改进,并将它们整合得很好。它非常努力地避免引入新奇性。
样板代码
Go 代码很快变得非常重复。它缺乏宏或泛型等机制来减少重复(接口对于抽象很好,但对于减少代码重复不太有效)。我经常会有很多函数,除了类型之外几乎都是相同的。
错误处理也导致了重复。许多函数的代码中,有着比有趣代码更多的 if err != nil { return err }
样板代码。
有时,使用泛型或宏来减少样板代码会因为使代码易于编写而受到批评,但使其变得更难阅读。但是我在 Go 中的体验是相反的。复制和粘贴代码非常快速和简单,但阅读 Go 代码却令人沮丧,因为你必须忽略掉太多内容或寻找微妙的不同之处。
我喜欢的东西
编译时间——绝对很快,绝对比 Rust 快得多。但它们实际上并不如我所期望的那么快。对于中大型项目来说,它对我来说感觉上与 C/C++ 差不多或稍快一点点,我原本期望会是即时编译。
Go 协程和通道——使用轻量级语法来创建 Go 协程和使用通道非常好用。这真的展示了语法的强大,这样的细节让并发编程比其他语言更容易。
接口——它们并不是很复杂,但易于理解和使用,在很多场景下都很有用。
if ...; ... { }
语法——能够将变量的作用域限定在 if
语句的主体内是很好的。这与 Swift 和 Rust 中的 if let
有类似的效果,但更通用。(Go 不像 Swift 和 Rust 那样具有模式匹配,所以无法使用 if let
。)
测试和文档注释——使用起来很容易。
Go
工具——将所有工具放在一个地方,而不需要在命令行上使用多个工具,这很不错。
垃圾收集器(GC)!——不用考虑内存管理真的让编程变得更容易。
Varargs。
我不喜欢的地方
并没有特定的顺序:
nil
切片 - nil
、一个 nil
切片和一个空切片是完全不同的 — 我觉得你只需要其中两种而不是三种。
没有一流的枚举类型 — 使用常量感觉有点落后。
不允许导入循环依赖 — 这会严重限制包的模块化,因为它鼓励将许多文件放在一个包内(或者有很多小的包,如果应该放在一起的文件没有被放在一起,那样也很糟糕)。
switch
语句可能不是完全穷尽的。
for ... range
返回索引/值对 — 获取索引很容易(只需忽略值),但只获取值需要显式处理。对我来说,这似乎是反着的,因为在大多数情况下我需要值而不是索引。
语法:
- 定义和使用之间的不一致性
- 编译器的挑剔性(例如,需要或禁止尾逗号);虽然良好的工具可以缓解这个问题,但仍然会有一些让人感到恼火的额外步骤。
- 使用多值返回类型时,类型需要在
return
语句中使用括号,但在return
语句中却不需要。 - 声明结构需要两个关键字(
type
和struct
)。 - 使用大写字母标记变量是公共还是私有。这有点像匈牙利命名法,但更糟糕。
隐式接口 — 我知道这在我的喜欢列表中,但有时真的很恼人,例如,当尝试找到所有实现某个接口的类型或给定类型实现了哪些接口时。
你不能为不同包中的接收器编写函数,因此即使接口是鸭子类型,也不能为上游类型实现它们,这使得它们变得不太有用。
我已经提到了上面的泛型和宏的缺失。
一致性
作为一名语言设计者和程序员,对我最让人意外的可能是 Go 内置和用户可用性之间的频繁不一致性。许多语言的目标是尽可能地消除魔术,并使内置功能对用户可用。运算符重载就是一个简单但有争议的例子。Go 中有很多魔术!你很容易就会遇到不能做的事情,而这些事情是内置功能可以做的。
以下是一些我注意到的事情:
有很好的语法可以返回多个值和使用通道,但这两者不能同时使用,因为没有元组类型。
有一个用于遍历数组和切片的 for ... range
语句,但是你不能遍历其他集合,因为没有迭代器的概念。
像 len
和 append
这样的函数是全局的,但没有办法使你自己的函数变成全局的。这些全局函数只能用于内置类型。它们也可以是泛型的,尽管 Go 据说没有泛型。
没有运算符重载。这在处理 ==
时特别令人恼火,因为这意味着你不能将自定义类型作为映射的键,除非它们是可比较的。这个特性是从类型的结构中派生出来的,无法由程序员覆盖。
结论
Go 是一种简单、小巧且令人愉快的语言。它有一些奇怪的地方,但大多数时候设计得很好。它学习起来非常快,避免了任何在其他语言中不为人知的特性。
Go 是一种与 Rust 完全不同的语言。尽管两者都可以模糊地描述为系统语言或 C 的替代品,但它们有不同的目标和应用场景、不同的语言设计风格和优先级。垃圾回收是一个巨大的不同点。Go 中有 GC 使得语言更简单、更小巧,更容易理解。Rust 中没有 GC 使得它非常快速(尤其是如果你需要确保延迟性,而不仅仅是高吞吐量),并且可以实现在 Go 中不可能的特性和编程模式(或者至少不会牺牲性能)。
Go 是一种编译型语言,具有良好实现的运行时。它很快。Rust 也是编译型语言,但运行时更小。它非常快。在没有其他约束的情况下,我认为选择使用 Go 还是 Rust 是在更短的学习曲线和更简单的程序(这意味着更快的开发速度)之间做出权衡,而 Rust 则非常非常快,并且具有更具表达力的类型系统(这使得您的程序更安全,并意味着更快的调试和错误排查)。
感谢阅读!