基础语法:变量、类型与模式匹配

🔊

上一篇我们聊了为什么 Rust 值得学,以及怎么跑通你的第一个 Hello World。这一篇直接上手语法——变量、类型、控制流、函数、模式匹配。核心目标只有一个:让你能读懂和写出 Rust 代码

如果你有 Go 的基础,这里的每个概念对你来说都不会陌生。我会在关键节点做对照,帮你把已知的知识映射过来。

变量声明

Rust 的变量声明非常简洁,核心原则是默认不可变,需要修改时加 mut

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn main() {
    // 不可变变量(默认)
    let x = 5;
    // x = 6;  // 编译错误!cannot assign twice to immutable variable

    // 可变变量
    let mut y = 5;
    y = 6;  // OK

    // 编译期常量
    const PI: f64 = 3.14159;

    // 变量遮蔽(Shadowing)
    let x = x + 1;  // 创建新绑定,覆盖旧值
    let x = "hello";  // 甚至可以改变类型
}

Go/Rust 对照:

GoRust
不可变绑定const x = 5 (包级) 或 x := 5 (函数内)let x = 5
可变绑定x := 5let mut x = 5
变量遮蔽x := 5; x := 10 (语法错误)let x = 5; let x = 10 (OK)
编译期常量const PI = 3.14const PI: f64 = 3.14

Go 没有真正意义上的不可变绑定——const 只能用于包级常量,函数内的 := 声明的变量默认可变。Rust 则是默认不可变,需要显式标注 mut 才能修改。

变量遮蔽是 Rust 的一个独特特性。Go 不允许同名变量遮蔽,但 Rust 允许通过新的 let 创建同名绑定,甚至可以改变类型。这在数据转换场景下很实用,比如先把字符串解析成数字,再进行计算。

const 在 Go 和 Rust 中都是编译期常量,但 Rust 的 const 必须显式标注类型。

基本数据类型

Rust 的类型系统在保留传统分类的同时,引入了更严格的类型约束:

类型类别Rust 类型说明
有符号整数i8, i16, i32, i64, i128, isizeisize = 平台相关有符号整数
无符号整数u8, u16, u32, u64, u128, usizeusize = 平台相关无符号整数
浮点数f32, f64IEEE 754 标准
布尔值booltrue / false
字符charUnicode 标量值(4 字节)
字符串&str, String字符串切片 / 堆分配字符串
元组(T1, T2, T3)固定长度异构集合
数组[T; N]固定长度同构集合

Go 开发者最该注意的差异是字符类型。Rust 的 char 是 Unicode 标量值(4 字节),可以表示任何 Unicode 字符,而 Go 的 rune 也是 4 字节的 Unicode 码点。但 Go 的 byteuint8 的别名(1 字节),Rust 没有单独的 byte 类型,直接用 u8

isizeusize 是平台相关的整数类型,对应指针大小——32 位系统上是 32 位,64 位系统上是 64 位。这和 Go 的 int / uint 类似。

字符串类型有两个:&str 是字符串切片(不可变, borrowed),String 是堆分配的字符串(可变,owned)。这和 Go 的 string(不可变)+ []byte(可变)的设计不同,Rust 通过借用规则来保证内存安全,不区分 UTF-8 字节序列和字符串类型。

元组类型用圆括号表示,比如 (i32, f64, bool)。Go 没有内置的元组类型,但可以通过结构体或多个返回值模拟。

rust
1
2
3
4
5
6
7
8
// 元组示例
let tuple: (i32, f64, bool) = (1, 3.14, true);
let (x, y, z) = tuple;  // 解构
let first = tuple.0;     // 通过索引访问

// 数组示例
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let first = arr[0];

控制流

if 表达式

Rust 的 if 是表达式——可以返回值:

rust
1
2
3
fn max(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

Go 的 if 是语句,不能返回值;Rust 的 if 是表达式。注意 if 表达式必须返回相同类型的值。

rust
1
2
3
4
5
6
// 条件必须是 bool 类型
let number = 6;
// if number {  // 编译错误!expected `bool`, found integer
if number != 0 {
    println!("number is non-zero");
}

Rust 的条件必须是 bool 类型,不会像 C/Go 那样自动把非零值当作 true

match 表达式

match 是 Rust 的杀手级特性,比 Go 的 switch 强大得多:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

// 模式匹配
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

// 范围匹配
fn classify(n: i32) -> &'static str {
    match n {
        0 => "零",
        1..=9 => "个位数",
        10..=99 => "两位数",
        _ => "其他",
    }
}

Go/Rust 对照:

GoRust
分支穿透默认穿透,需 break 阻止默认不穿透,每个分支独立
必须穷尽不必,有 default必须,编译器检查
模式匹配有限(type switch)强大(结构、范围、Option 等)
表达式不是表达式是表达式,可返回值

Rust 的 match 必须穷尽所有可能,否则编译失败。这保证了不会遗漏任何情况,避免了 Go 中常见的"忘记 default"的 bug。

match 的每个分支用 => 连接模式和表达式。_ 是通配符,匹配所有未列出的情况。范围匹配用 ..= 表示闭区间,比 Go 的 case 更直观。

Option<T> 是 Rust 的标准库类型,表示"有值或无值",比 Go 的 (T, error) 更轻量。Some(T) 表示有值,None 表示无值。通过 match 可以优雅地处理这两种情况。

循环

Rust 有三种循环:loopwhilefor

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fn main() {
    // loop —— 无限循环,必须用 break 跳出
    let mut count = 0;
    let result = loop {
        count += 1;
        if count == 10 {
            break count * 2;  // break 可以返回值
        }
    };

    // while —— 条件循环
    let mut i = 0;
    while i < 10 {
        println!("{}", i);
        i += 1;
    }

    // for —— 迭代器循环
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is: {}", element);
    }

    // for 范围
    for number in 1..4 {  // 1, 2, 3(不包括 4)
        println!("{}", number);
    }

    for number in 1..=4 {  // 1, 2, 3, 4(包括 4)
        println!("{}", number);
    }
}

Go/Rust 对照:

场景GoRust
无限循环for { ... }loop { ... }
条件循环for condition { ... }while condition { ... }
遍历集合for i, v := range itemsfor (i, v) in items.iter().enumerate()
索引循环for i := 0; i < n; i++for i in 0..n

Rust 的 for 循环基于迭代器,比 Go 的 range 更通用。通过迭代器模式,可以遍历任何实现了 IntoIterator trait 的类型。

loop 是 Rust 的特色——无限循环必须有显式的 break,这避免了 Go 中常见的"忘记 break"的死循环 bug。break 甚至可以返回值,这在某些场景下很实用。

函数

Rust 用 fn 关键字定义函数,参数必须有类型标注,返回值类型在 -> 后:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 基本函数
fn add(a: i32, b: i32) -> i32 {
    a + b  // 最后一个表达式(无分号)是返回值
}

// 多返回值(通过元组)
fn divide(a: f64, b: f64) -> (f64, f64) {
    (a / b, a % b)
}

fn main() {
    let result = add(1, 2);
    let (quotient, remainder) = divide(10.0, 3.0);
}

Go/Rust 对照:

特性GoRust
函数签名func add(a, b int) intfn add(a: i32, b: i32) -> i32
多返回值原生支持通过元组
命名返回值支持不支持
返回值省略可省略(有返回值类型)最后一个表达式自动返回

Rust 没有命名返回值,但通过表达式返回值更简洁。Go 的命名返回值可以在函数体中直接赋值,但 Rust 需要显式返回。

语句 vs 表达式

这是 Rust 和 Go 最大的语法差异之一。Rust 区分语句(statement)和表达式(expression):

  • 语句:执行某些操作但不返回值,以分号 ; 结尾
  • 表达式:计算并返回值,不以分号结尾
rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 语句(Statement)
let y = 6;  // let 是语句,不返回值
// let x = (let y = 6);  // 编译错误!expected expression, found statement

// 表达式(Expression)
{
    let x = 3;
    x + 1  // 这是表达式,返回 4
}

// if 是表达式
let number = if condition { 5 } else { 6 };

// loop 可以返回值
let result = loop {
    break 100;
};

Go 中几乎所有东西都是语句,只有少数例外(如 func literal)。Rust 中 ifmatchloop{} 块都可以是表达式,这让代码更简洁。

关键规则:给变量赋值的表达式不能以分号结尾,否则就变成了语句,返回 ()(unit type)。

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 错误示例
let x = {
    let y = 3;
    y + 1;  // 加了分号,变成了语句,返回 ()
};
// x 的类型是 (),不是 i32

// 正确示例
let x = {
    let y = 3;
    y + 1  // 无分号,返回 4
};

引用与切片基础

Rust 的引用系统是其所有权模型的基础。引用允许你在不获取所有权的情况下访问值:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 借用 s1
    println!("The length of '{}' is {}.", s1, len);  // s1 仍然有效

    let s2 = String::from("hello world");
    let hello = &s2[0..5];  // 字符串切片
    let world = &s2[6..11];
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

&s1 创建对 s1 的不可变引用,s 的类型是 &String。函数体结束,引用失效,但 s1 的所有权不变。

切片语法 [start..end] 创建对集合的切片视图。[0..5] 表示从索引 0 开始(包含)到索引 5 结束(不包含)。.. 可以省略开始或结束:..5 表示从 0 到 5,6.. 表示从 6 到末尾。

Go/Rust 对照:

GoRust
不可变引用&T 或指针 *T&T
可变引用*T&mut T
字符串切片s[start:end]&s[start..end]
借用规则无约束同一作用域内,多个不可变引用 或 一个可变引用

Go 没有借用规则,可以同时有多个可变引用。Rust 的借用规则保证了编译时内存安全,这是 Rust 最核心的设计之一。

所有权入门预告

Rust 的所有权(ownership)是其最独特的特性,确保内存安全而无需垃圾回收。基本规则:

  1. Rust 中的每个值都有一个所有者(owner)
  2. 值在任一时刻有且只有一个所有者
  3. 当所有者离开作用域,值被丢弃
rust
1
2
3
4
5
6
7
8
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的所有权移动到 s2
    // println!("{}", s1);  // 编译错误!value borrowed here after move

    let s3 = s2.clone();  // 深拷贝,s2 仍然有效
    println!("{}", s2);
}

字符串类型 String 是堆分配的,赋值时默认是移动(move)语义,不是浅拷贝。Go 的字符串赋值是浅拷贝(共享底层数组),Rust 需要显式调用 .clone() 才能深拷贝。

所有权的完整内容我们留到下一篇详细讲,包括借用(borrowing)、生命周期(lifetime)等核心概念。这里先有个印象即可——Rust 通过所有权在编译时保证内存安全,不需要垃圾回收。

本篇小结

到此我们已经覆盖了 Rust 基础语法的核心——变量声明、基本数据类型、控制流、函数、语句 vs 表达式、引用与切片、所有权入门。你现在已经能读懂大部分 Rust 代码了。

如果记住一句话就够了:Rust 的语法兼顾了表达力和安全性——变量默认不可变,match 强大且必须穷尽,if/match/loop 都是表达式,借用规则在编译时保证内存安全。

下一篇我们深入 Rust 最独特的特性:所有权与借用——所有权规则、引用、生命周期、Copy trait。这是 Rust 安全编程的基石,也是 Go 开发者最需要适应的地方。

参考资源