对比学习 Go 和 Rust:介绍

2022年1月4日 // Go Rust 编程

介绍

Go 和 Rust 首次公开分别是 2009 年和 2010 年,是两种相对比较新的语言,在发布时都自称为系统级编程语言,并在发布后都获得了广泛的关注和应用。两者的特征和应用领域有一定的重合,但也差异显著。许多人在最初会选择其中一个,但后来往往由于特定的应用需求,或仅仅出于好学,会进一步选择另外一种语言。对于像我这样水平一般的人,同时学习多种语言是比较困难的,经常是学会一个而忘记另外一个。为了能同时学会这两种语言,最好是对两者的各个关键知识点进行深入的对比,弄清这些关键知识点的主要区别、设计初衷、优缺点、惯用法等。这种学习方法会非常累人,但也卓有成效。在学习过程中,我将会把学习成果整理为你所看到的这个《对比学习 Go 和 Rust》的系列学习笔记。希望通过此笔记整理自己的学习成果,供自己日后查阅,同时也希望能为同样同时学习 Go 和 Rust 的朋友提供参考。

鉴于目前已经有很多关于 Go 和 Rust 语言的学习资料了,此系列笔记将会略过一些简单的内容,尽量做到简明扼要,不强调系统性,不去深挖语言的实现原理,也没有循序渐进的特点。因此,此系列笔记只能作为同时学习这两门语言的辅助材料,且不适合初学者。由于 Go 相比 Rust 要简单很多,此系列学习笔记尤其适合那些已经学会 Go,又想进一步学习 Rust 的朋友阅读。

简单对比

关于这两种语言的介绍资料已经有很多了,这里只给出两句话介绍:

  • Go 是一个通用的系统级编程语言,具有表达能力强、简洁、干净和高效的特点,可用于构建大型、快速、可靠和高效的软件。
  • Rust 也是一种通用的系统级编程语言,具有高性能、可靠和生产力强的特点,可用于构建可靠且高效的软件。

Go 和 Rust 有许多共同的特征,两者同时配享有如下标签:现代、开源、通用、编译型、强类型、表达力强、高性能、高并发、内存安全、模块化、可扩展、跨平台,等等。

两者的差异主要表现在:

  • 简单性: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 的确存在许多不必要的复杂。要不然,就冲 Rust 的许多绝杀技,我会毫不犹豫地选择 Rust。

如果要在两种语言间进行选择的话,我自己会在够用的前提下,尽量选择 Go 语言,因为它真的更简单易学,这也使其利于团队间的交流协作。但很多时候,Go 会不够用,如需要编写要求极致性能的程序,需要精确地调度计算资源,需要使用 WebAssembly 技术编写复杂且高性能的 Web 应用,或者进行嵌入式开发,这时用 Rust 将会是更好的选择。如果不考虑学习成本,正好我们同时精通这两种语言,在实际开发中,我们应该更倾向于选择 Rust,毕竟 Rust 在更多方面都做得更好。当然,同时学会这两种语言,分别将他们应用于不同的场景中,这将会更好,只是付出的代价也相对较大而已。

安装和更新

这里只介绍在 Linux 操作系统下的安装和更新。许多 Linux 发行版的软件仓库中已经有 Go 或 Rust 了,但常常又不是最新版本,因此这里介绍如何手动安装最新版本。

安装

Go 语言的安装步骤:

  1. 官网(或国内镜像)下载对应版本、对应操作系统、对应处理器架构的安装包,如 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
    
  2. /usr/local/go/bin 添加到 PATH 环境变量列表中,这可通过在 $HOME/.profile/etc/profile(对操作系统全局安装时选择)文件中添加如下一行:

    export PATH=$PATH:/usr/local/go/bin
    

    可通过运行 source $HOME/.profile 使以上改动快速生效。

  3. 通过运行以下命令验证安装是否成功:

    $ go version
    

    如果打印正确的版本,如 go version go1.17.6 linux/amd64,则表明安装成功。

Rust 语言的安装步骤:

  1. 类 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 使以上改动快速生效。

  2. 通过运行以下命令验证安装是否成功:

    $ 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.gogo.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
        }

        fmt.Printf("You win: %v\n", guess)

        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 的表现也不完全一样。你可以在一行中同时输入用空格分隔的两个数字看看情况,或者输入非数字试试。