介绍
Go 和 Rust 首次公开分别是 2009 年和 2010 年,是两种相对比较新的语言,在发布时都自称为系统级编程语言,并在发布后都获得了广泛的关注和应用。两者的特征和应用领域有一定的重合,但也差异显著。许多人在最初会选择其中一个,但后来往往由于特定的应用需求,或仅仅出于学习兴趣,会进一步选择另外一种语言。对于像我这样水平一般的人,同时学习多种语言是比较困难的,经常是学会一个而忘记另外一个。为了能同时学会这两种语言,最好是对两者的各个关键知识点进行深入的对比,弄清这些关键知识点的主要区别、设计初衷、优缺点、惯用法等。这种学习方法会非常累人,但也卓有成效。在学习过程中,我将会把学习成果整理为你所看到的这个《对比学习 Go 和 Rust》的系列学习笔记。希望通过此笔记整理自己的学习成果,供自己日后查阅,同时也希望能为同样同时学习 Go 和 Rust 的朋友提供参考。
鉴于目前已经有很多关于 Go 和 Rust 语言的学习资料了,此系列笔记将会略过一些简单的内容,尽量做到简明扼要,不强调系统性,不去深挖语言的实现原理,也没有循序渐进的特点。因此,此系列笔记只能作为同时学习这两门语言的辅助材料,且不适合初学者。由于 Go 相比 Rust 要简单很多,此系列学习笔记尤其适合那些已经学会 Go,又想进一步学习 Rust 的朋友阅读。
简单对比
关于这两种语言的介绍资料已经有很多了,这里只给出两句话介绍:
- Go 是一个通用的系统级编程语言,具有表达能力强、简洁、干净和高效的特点,可用于构建大型、快速、可靠和高效的软件。
- Rust 也是一种通用的系统级编程语言,具有高性能、可靠和生产力强的特点,可用于构建可靠且高效的软件。
Go 和 Rust 有许多共同的特征,两者同时配享有如下标签:现代、开源、通用、编译型、强类型、表达力强、高性能、高并发、内存安全、模块化、可扩展、跨平台,等等。
要更加深入地理解两个语言的不同,更应该回溯两者的推出动机。Go 的推出主要是为了解决以下痛点:现有的编译型和静态类型语言普遍存在复杂、难以使用、编译速度慢等问题,而解释型和动态类型语言则存在运行速度慢、非类型安全、对大型复杂项目的适用性差的问题。Go 主要在简单性、类型安全、编译速度快、运行效率高、易于实现并发方面取得了成功。而 Rust 推出的动机则更为单纯,它是在保留 C/C++ 高效和低层控制能力的同时,解决这两个语言存在的安全性和便利性问题。
两者的差异主要表现在:
- 简单性:Go 很简单,在特性选择上属于够用就好,很多特性像动态脚本语言一样灵活,因此易学易用;Rust 则复杂得多,包含许多强大且有用的特性,因此具有较多的编程范式,实现相同功能其代码量会更少,但难以学习。
- 性能:两者都被编译为原生机器码,因此 Go 不弱,但 Rust 很强;Rust 的设计遵循零开销抽象的原则,使其能在特性丰富的同时保持高性能,实属不易。
- 编译速度:Go 很快,Rust 比较慢。
- 内存安全:Go 通过自动垃圾回收实现类型安全,而 Rust 则创新性地通过所有权系统、借用和生命周期实现类型安全,。
- 并发支持:对并发的原生支持是 Go 的重要特色,Rust 当然也可以进行并发编程,但不如 Go 成熟和容易;不过 Go 的并发主要适用于高吞吐量的网络服务这一应用领域,对于其他领域的并行计算,Go 反倒不如 Rust。
- 可控性:Rust 更接近计算机底层,能更精确地管理计算机内存及其他资源。
- 一致性:Go 语言的简单看起来有点像黑魔法,其内部实现和语言要求是不一致的,Rust 则更为一致。
- 错误处理:Go 繁琐的错误处理方法经常引起争议,Rust 在这方面则较少争议,不过两者的底层思想其实差异不大。
- 生态:相对于 Rust,Go 更成熟,相关代码库的数量也更多,因此生态更好。
- 应用领域:Go 常用来写 Web 应用、数据库开发、网络编程。Rust 适合用来写命令行、网络服务、WebAssembly、嵌入式程序,等等。其中命令行和网络服务用 Go 也完全可以胜任,在 WebAssembly 领域 Go 不如 Rust,而在嵌入式领域 Go 则更难胜任。
这两种语言的编程思想存在较大的差异。Go 语言的编程思想基本上可用“大道至简”来形容,即只往语言中添加一些必不可少的特性,从而使其保持简洁和干净。而 Rust 则为了实现内存安全、高性能、零开销抽象等目标而绞尽脑汁,可谓无所不用其极。这导致 Rust 看起来有点怪异、学究气,学习曲线陡峭,不过好在 Rust 已经构建了一套完整的体系,实现了自己的目标。
如何选择?
许多人在 Go 和 Rust 之间进行选择时,都会首先问哪个语言更好。这是个见仁见智的问题,不同的人往往会有不同的答案。简单想一想,这两种语言之所以变得越来越流行,是因为针对这两种语言所做的创新性努力得到了人们的认可,能够应用他们解决一些实际痛点,两者都是优秀的语言。
个人看来,要在这两种语言之间进行选择,主要应该考虑应用场景、开发速度和掌控能力。
首先说应用场景,Go 和 Rust 的应用领域虽然有重合,但也有较大差异。Go 要更偏重业务开发,而 Rust 更偏重底层技术。因此,如果你的应用领域更偏底层、对性能敏感,如操作系统、驱动开发、游戏引擎、WebAssembly、嵌入式,那么就选 Rust;而其他则可以同时选择 Go 或 Rust。
其次,在开发速度上,尽管 Rust 也声称能够快速开发,但由于该语言本身的复杂性,实际上其开发速度是不如 Go 的,如果你正在开发一个需要快速开发和部署的系统,尽量还是选择 Go。
最后,要考虑自己和团队对语言的掌控能力。不要相信网上经常有人声称的区区数天精通某种编程语言的炫耀,其实精通任何一门语言都不是那么容易的,需要长期的主动学习,更何况 Rust 是以学习曲线陡峭著称的。因此,当你决心用 Rust 语言时,很可能面临一个很现实的问题:你或你的团队没有足够的精力、能力或毅力来真正学会这个语言。相对来说,Go 的学习成本则低很多。
安装和更新
这里只介绍在 Linux 操作系统下的安装和更新。许多 Linux 发行版的软件仓库中已经有 Go 或 Rust 了,但常常又不是最新版本,因此这里介绍如何手动安装最新版本。
安装
Go 语言的安装步骤:
-
从官网(或国内镜像)下载对应版本、对应操作系统、对应处理器架构的安装包,如
go1.17.6.linux-amd64.tar.gz
,将其内容解压到/usr/local
目录中,这将创建/usr/local/go
目录树:$ wget https://golang.google.cn/dl/go1.17.6.linux-amd64.tar.gz $ sudo rm -rf /usr/local/go $ sudo tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz
-
将
/usr/local/go/bin
添加到PATH
环境变量列表中,这可通过在$HOME/.profile
或/etc/profile
(对操作系统全局安装时选择)文件中添加如下一行:export PATH=$PATH:/usr/local/go/bin
可通过运行
source $HOME/.profile
使以上改动快速生效。 -
通过运行以下命令验证安装是否成功:
$ go version
如果打印正确的版本,如
go version go1.17.6 linux/amd64
,则表明安装成功。
Rust 语言的安装步骤:
-
类 Unix 系统中一般运行如下命令通过
rustup
脚本安装 Rust:$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
这将在
$HOME/.cargo
目录装安装 Rust。以上命令同时会自动将 Rust 的二进制文件路径添加到PATH
环境变量中, 通常在.profile
文件的末尾可以看到如下一行:export PATH="$HOME/.cargo/bin:$PATH"
可通过运行
source $HOME/.profile
使以上改动快速生效。 -
通过运行以下命令验证安装是否成功:
$ cargo --version
如果打印正确的版本,如
cargo 1.57.0 (b2e52d7ca 2021-10-21)
,则表明安装成功。
更新
要更新 Go,请重复以上安装步骤的第一步。即直接下载最新的安装包,并覆盖已有的安装包。
要更新 Rust,请运行如下命令:
$ rustup update
卸载
要卸载 Go,请首先删除 /usr/local/go
目录:
$ sudo rm -rf /usr/local/go
然后再删除 $HOME/.profile
或 /etc/profile
文件中的如下一行:
export PATH=$PATH:/usr/local/go/bin
要卸载 Rust,请运行如下命令:
$ rustup self uninstall
Hello, world!
程序
就像大多数初次入门教程那样,奉上这两门语言的 Hello, world!
程序代码。
Go:
package main
import "fmt"
func main() {
fmt.Printf("Hello, world!\n")
}
Rust:
fn main() {
println!("Hello, world!");
}
猜数字游戏
在官方的Rust 程序设计语言一书中,在第 2 章专门详细介绍了一个猜数字游戏。本章再次列出该程序代码,同时用 Go 实现相同的程序,为的是通过对比,能初步了解两种语言的异同。
Rust
以下是用 Rust 实现的猜数字游戏的构建过程:
$ cargo new guessing_game
$ cd guessing_game
$ echo 'rand = "0.9.0"' >> Cargo.toml
$ code src/main.rs # 并在 main.rs 中输入下面的代码
$ cargo run
Rust 程序设计语言中的猜数字游戏:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Go
以下是用 Go 实现的猜数字游戏的构建过程:
$ mkdir guessinggame
$ cd guessinggame
$ touch main.go
$ go mod init example.com/guessinggame
$ code main.go # 并在 main.go 中输入下面的代码
$ go run .
这样在 guessinggame
目录将存在两个文件,main.go
和 go.mod
,其中 go.mod
文件的内容如下:
module example.com/guessinggame
go 1.17
接下来就是在 main.go
文件中写代码了,这里直接填写最终的代码:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
fmt.Println("Guess the number!")
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(101)
Loop:
for {
fmt.Println("Please input your guess.")
guess := -1
_, err := fmt.Scanln(&guess)
if err != nil {
fmt.Println("Failed to read line:", err)
continue
}
switch {
case guess < secretNumber:
fmt.Println("Too small!")
case guess > secretNumber:
fmt.Println("Too big!")
case guess == secretNumber:
fmt.Println("You win!")
break Loop
}
}
}
其实即便是这种简单的程序,前面的 Rust 和后面的 Go 的表现也不完全一样。你可以在一行中同时输入用空格分隔的两个数字看看情况,或者输入非数字试试。