09-函数

面向熟悉 Go 的读者讲清 Rust 函数签名、返回值、所有权、泛型与可调用对象差异

函数 (Functions)

面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。

热度: hot 高频 | common 常见 | occasional 偶尔 | advanced 进阶少见

本篇能解决什么:

  • 你会不会总把 Rust 函数当成 Go 的 func 来看,结果一写就撞上类型、分号、返回值规则?
  • 你是否想知道:Rust 为什么没有 Go 那样的“多返回值”语法,却照样能优雅返回多项结果?
  • 你会不会在“参数传进去以后变量怎么没了”这个问题上,被所有权和借用搞糊涂?
  • 你是否分不清“函数项、函数指针、闭包”到底是不是一回事,什么时候能互相替代?
  • 你是否想搞懂泛型函数、impl Trait、turbofish 到底各自解决什么问题?
  • 你会不会遇到 E0308、E0382、E0277 这类函数相关报错,却看不出编译器在阻止什么坑?

术语速查表:

术语 / 缩略词全称 / 读法中文一句话解释Go 里的近亲
function—函数一段可复用的可调用代码,有参数、返回值和函数体func
signature—函数签名函数名、参数类型、返回类型这一层“接口”func f(a int) string 的声明头
parameter—形参函数定义时写在括号里的名字和类型形参
argument—实参调用函数时传进去的具体值实参
return type—返回类型-> T 指明函数调用结束后交出的值类型Go 返回列表
expression—表达式会产生一个值的代码片段,如 1 + 2、if、matchGo 普通表达式;但 Go 的 if 不是表达式
statement—语句执行动作但不产出可继续使用的值Go 语句
unit—单元类型(),表示“没有有意义的数据”void / 空结构用途接近
owner—所有者当前负责在作用域结束时释放该值的绑定Go 无对应物
move—移动 / 转移所有权把值的所有权交给新变量或函数,旧绑定随即失效Go 没有这条规则
borrow—借用临时把值“借出去看”或“借出去改”,不转移所有权传指针最像,但规则更严格
Copy—按位复制 trait标记赋值/传参时直接复制的小类型Go 大多数值类型的默认行为
Clone—克隆 trait显式复制一份新值,可能有堆分配成本手写深拷贝
tuple—元组固定长度、可异构的数据包,可当“多返回值替身”Go 没有直接等价物
generic—泛型用类型参数把一份函数逻辑复用到多种类型Go 1.18+ 泛型
monomorphization/məˌnɒməfɪˈkeɪʃən/单态化编译器把泛型函数按实际类型展开成多份具体代码Go 编译器也会做类似专门化/字典混合实现,但细节不同
turbofish::<...>turbofish 语法显式写出类型参数,帮助编译器推断Go 没有专门叫法
impl Trait—impl Trait 语法用 trait 约束“某个实现了该能力的类型”,可出现在参数或返回位置Go 接口参数最像
trait—trait / 特征一组方法或能力约束interface
function item—函数项某个已命名函数本身的零大小唯一类型命名函数值
function pointerfn(...) -> ...函数指针类型只指向函数代码、不捕获环境的可调用值func(int) int 最像
closure—闭包能捕获外部环境的匿名可调用对象Go 闭包
associated function—关联函数写在 impl 里但没有 self 的函数,常作构造器类型上的普通函数
method—方法第一个参数是 self / &self / &mut self 的函数方法
diverging function—发散函数永远不会正常返回到调用点的函数panic / 无限循环函数
never type!never 类型表示“这个表达式永远没有值返回”Go 无对应物
const fn—常量函数既可像普通函数调用,也可在常量上下文求值Go 没有对应语法
variadic—可变参数参数个数可变的函数形式Go ...T

说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。

热度索引:

热度题目
hotQ1, Q2, Q3, Q4, Q5, Q6, Q7, Q8, Q9, Q19
commonQ10, Q11, Q12, Q13, Q14, Q15, Q20
occasionalQ16, Q17
advancedQ18

Q1. Rust 函数最基本的写法是什么?为什么参数类型一个都不能省?

Tags: hot beginner fn 适用版本: Rust 1.0+

一句话答案:

Rust 用 fn 名字(参数: 类型, ...) -> 返回类型 { ... } 定义函数;参数类型必须全部写明,因为函数签名是公开契约,Rust 不在这里做 Go 式的“从用法反推参数类型”。

解答:

先看最小可用写法。这里的函数签名(signature,函数名、参数类型、返回类型这一层接口)由 fn add(a: i32, b: i32) -> i32 构成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn greet(name: &str) {
    println!("hello, {name}");
}

fn main() {
    println!("{}", add(2, 3));
    greet("Rust");
}

无返回值函数的真实返回类型其实是 单元类型(unit)(),只是经常省略:

1
2
3
4
5
6
7
8
fn log_number(n: i32) -> () {
    println!("n = {n}");
}

fn main() {
    let value: () = log_number(7);
    assert_eq!(value, ());
}

「❌ 错误写法」——把 Go 的“同类型参数挤在一起写”带过来:

1
2
3
4
5
fn main() {
    // fn add(a, b: i32) -> i32 { a + b }
    // error: expected parameter name, found `:`
    // 在 Rust 里,每个参数都必须单独写成 `名字: 类型`
}

Rust 要你把类型写全,是因为函数定义点本身就要足够清晰,不能等调用点再猜。这样 IDE、文档、trait 实现匹配、错误提示都会更稳定。

「✅ 正确写法」——每个参数各写各的类型:

1
2
3
4
5
6
7
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    assert_eq!(add(10, 20), 30);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func greet(name string) {
	fmt.Println("hello,", name)
}

func main() {
	fmt.Println(add(2, 3))
	greet("Go")
}
  • Go 怎么做:同类型参数可合并写成 a, b int,无返回值直接省掉返回列表。
  • Rust 为什么不同:Rust 把签名看成强约束接口,要求每个参数都显式写出类型,避免二义性并让类型系统工作更直接。
  • Go 程序员易踩的坑:最容易把 fn f(a, b: i32) 当成合法写法;Rust 不接受。

记忆点:

  • Rust 参数列表里没有“共享类型尾巴”写法。
  • 无返回值函数本质上返回 ().
  • 函数签名是 Rust 类型系统的重要边界,写全不是啰嗦,是规则。

Q2. Rust 函数为什么常常“不写 return”?分号到底在控制什么?

Tags: hot beginner expression 适用版本: Rust 1.0+

一句话答案:

Rust 是表达式语言:函数体最后一个“没有分号的表达式”就是返回值;多写一个分号,它就从“有值”变成“只执行动作的语句”,返回值也就变成 () 了。

解答:

先区分两个词:表达式(expression,会产生值的代码)和语句(statement,只执行动作、不继续提供值的代码)。Rust 的 if、match、块 { ... } 都可以是表达式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn square(n: i32) -> i32 {
    n * n // 最后一个无分号表达式,直接作为返回值
}

fn abs(n: i32) -> i32 {
    if n >= 0 {
        n
    } else {
        -n
    }
}

fn main() {
    assert_eq!(square(4), 16);
    assert_eq!(abs(-5), 5);
}

你当然也可以显式写 return,尤其在“提前退出”时很清楚:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn classify(n: i32) -> &'static str {
    if n < 0 {
        return "negative";
    }
    "non-negative"
}

fn main() {
    assert_eq!(classify(-1), "negative");
    assert_eq!(classify(8), "non-negative");
}

「❌ 错误写法」——最后一行多写了分号,返回值从 i32 变成了 ():

1
2
3
4
5
6
7
fn main() {
    // let value: i32 = { 5; };
    // error[E0308]: mismatched types
    //  expected `i32`, found `()`
    let value: i32 = 5;
    let _ = value;
}

块表达式也遵守同一条规则:

1
2
3
4
5
6
7
fn main() {
    let x = {
        let base = 3;
        base + 1
    };
    assert_eq!(x, 4);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func square(n int) int {
	return n * n
}

func main() {
	x := func() int {
		base := 3
		return base + 1
	}()
	fmt.Println(square(4), x)
}
  • Go 怎么做:普通函数必须显式 return;if 不是表达式,块也不会自然产出值。
  • Rust 为什么不同:Rust 让控制流结构本身参与“产值”,这样很多逻辑可以更紧凑,而且编译器能直接检查各分支类型是否一致。
  • Go 程序员易踩的坑:把分号当成无害结束符;在 Rust 里,它真的会改变类型和返回值。

记忆点:

  • 最后一行想返回值,就别写分号。
  • return 主要用于提前退出,不是每个函数结尾都必须写。
  • if、match、块都可以产生值。

Q3. Rust 能像 Go 那样返回多个值吗?该用元组还是结构体?

Tags: hot beginner tuple 适用版本: Rust 1.0+

一句话答案:

Rust 没有 Go 那种“返回值列表”语法,但可以返回元组(tuple,固定长度、可异构的数据包)来达到同样效果;当字段含义开始变多、变重要时,就该改用结构体。

解答:

最直接的写法就是返回元组,再在调用处解构:

1
2
3
4
5
6
7
8
9
fn div_rem(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}

fn main() {
    let (q, r) = div_rem(17, 5);
    assert_eq!(q, 3);
    assert_eq!(r, 2);
}

不需要的部分可以用 _ 忽略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn min_max(xs: &[i32]) -> (i32, i32) {
    let mut min = xs[0];
    let mut max = xs[0];
    for &x in xs {
        if x < min {
            min = x;
        }
        if x > max {
            max = x;
        }
    }
    (min, max)
}

fn main() {
    let (_, max) = min_max(&[3, 9, 1, 5]);
    assert_eq!(max, 9);
}

字段一多,元组就开始“靠位置猜含义”。这时结构体更稳:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[derive(Debug, PartialEq)]
struct ParseStats {
    count: usize,
    sum: i32,
}

fn parse_stats() -> ParseStats {
    ParseStats { count: 3, sum: 60 }
}

fn main() {
    let stats = parse_stats();
    assert_eq!(stats, ParseStats { count: 3, sum: 60 });
}

如果还可能失败,就很常见地写成 Result<(T, U), E>:

1
2
3
4
5
6
7
fn parse_pair(a: &str, b: &str) -> Result<(i32, i32), std::num::ParseIntError> {
    Ok((a.parse()?, b.parse()?))
}

fn main() {
    assert_eq!(parse_pair("10", "20").unwrap(), (10, 20));
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func divRem(a, b int) (int, int) {
	return a / b, a % b
}

type ParseStats struct {
	Count int
	Sum   int
}

func main() {
	q, r := divRem(17, 5)
	fmt.Println(q, r)

	stats := ParseStats{Count: 3, Sum: 60}
	fmt.Println(stats)
}
  • Go 怎么做:语言原生支持多返回值,尤其常见于“值 + error”。
  • Rust 为什么不同:Rust 把“一个函数调用的结果”统一看成一个值;这个值可以是元组、结构体、Result、Option 等。
  • Go 程序员易踩的坑:总想找 (x, y) 以外的“多返回值语法糖”;其实 Rust 的惯用答案就是“返回一个复合值”。

记忆点:

  • 少量结果:优先元组。
  • 结果字段有名字或语义较强:优先结构体。
  • “成功多值 / 失败报错”:常写 Result<(T, U), E>。

Q4. Rust 的 if / match 为什么能直接当返回值?

Tags: hot beginner if match 适用版本: Rust 1.0+

一句话答案:

因为 Rust 的 if 和 match 本身就是表达式;只要各分支类型一致,它们整个块就能产出一个值,直接作为函数返回值。

解答:

最常见的是把 if 当成返回值生成器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn sign(n: i32) -> &'static str {
    if n > 0 {
        "positive"
    } else if n < 0 {
        "negative"
    } else {
        "zero"
    }
}

fn main() {
    assert_eq!(sign(0), "zero");
}

match 更适合“枚举所有情况”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn grade(score: u8) -> &'static str {
    match score {
        90..=100 => "A",
        75..=89 => "B",
        60..=74 => "C",
        _ => "D",
    }
}

fn main() {
    assert_eq!(grade(88), "B");
}

「❌ 错误写法」——分支类型不一致:

1
2
3
4
5
6
fn main() {
    // let value: i32 = if true { 1 } else { "nope" };
    // error[E0308]: `if` and `else` have incompatible types
    let value: i32 = if true { 1 } else { 0 };
    let _ = value;
}

即使写成很多层判断,规则仍然一样:整个表达式只有一个统一类型。某个分支如果是 never 类型(!,表示永远不会返回到这里),也可以和别的类型统一,这会在 Q18 里讲。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func sign(n int) string {
	if n > 0 {
		return "positive"
	} else if n < 0 {
		return "negative"
	}
	return "zero"
}

func main() {
	fmt.Println(sign(0))
}
  • Go 怎么做:if / switch 只负责控制流,不直接产出值,只能在分支里写 return 或给外部变量赋值。
  • Rust 为什么不同:控制流就是表达式后,很多局部逻辑不必先声明可变变量再赋值,代码更贴近“把这个值算出来”。
  • Go 程序员易踩的坑:会本能地先 let mut x,再在每个分支给它赋值;Rust 里很多时候不必这样写。

记忆点:

  • if / match 在 Rust 里都是值。
  • 各分支类型必须统一。
  • 能直接返回时就直接返回,通常比“先声明再赋值”更自然。

Q5. 函数参数传进去时,所有权到底会发生什么?

Tags: hot beginner ownership E0382 适用版本: Rust 1.0+

一句话答案:

参数传递默认也是“赋值”规则:对非 Copy 类型,传给 fn f(x: T) 会 move(转移所有权);传给 fn f(x: &T) 则只是借用(borrow),调用方还能继续用原值。

解答:

先看“被吃掉”的版本。这里 String 没有实现 Copy,所以把它按值传入函数会把所有权交给函数:

1
2
3
4
5
6
7
8
9
fn consume(name: String) {
    println!("got {name}");
}

fn main() {
    let s = String::from("Rust");
    consume(s);
    // s 已经失效,不能再用
}

「❌ 错误写法」——传给按值参数后还想继续用原变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn consume(name: String) {
    println!("{name}");
}

fn main() {
    let s = String::from("Rust");
    consume(s);
    // println!("{s}");
    // error[E0382]: borrow of moved value: `s`
}

如果你只是想读它,不想拿走所有权,就借用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn print_name(name: &str) {
    println!("name = {name}");
}

fn main() {
    let s = String::from("Rust");
    print_name(&s); // &String 会自动转成 &str
    print_name("Go"); // 字符串字面量本来就是 &str
    println!("{s}"); // 仍然可用
}

对于实现了 Copy 的小类型,如 i32、bool,按值传递只是复制一份,不会让原变量失效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn plus_one(n: i32) -> i32 {
    n + 1
}

fn main() {
    let x = 10;
    let y = plus_one(x);
    assert_eq!(x, 10);
    assert_eq!(y, 11);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func mutate(xs []int) {
	xs[0] = 99
}

func main() {
	s := "Rust"
	fmt.Println(s) // 字符串按值传,但原变量始终可继续用

	xs := []int{1, 2, 3}
	mutate(xs)      // slice 头按值复制,但底层数组共享
	fmt.Println(xs) // [99 2 3]
}
  • Go 怎么做:参数永远是按值传,但很多值本身是“头部 + 指向底层数据”的结构,所以看起来像“函数里改了外面也变了”。
  • Rust 为什么不同:Rust 把“谁负责释放资源”也纳入函数调用规则,因此按值参数会接管所有权,避免悬垂指针和二次释放。
  • Go 程序员易踩的坑:以为“传参只是复制一个头”;在 Rust 里对 String、Vec<T> 不是这样,函数真的会把值吃掉。

记忆点:

  • fn f(x: T):函数拿走 x 的所有权(若 T 非 Copy)。
  • fn f(x: &T):函数只是借用。
  • 遇到 E0382,先问自己“我是不是其实只想读它?”

Q6. Rust 函数返回值时,所有权又是怎么交回来的?

Tags: hot beginner return ownership 适用版本: Rust 1.0+

一句话答案:

返回值本质上也是一次 move:函数把某个值的所有权交给调用方;因此“把参数吃进去再原样返回”是合法且常见的所有权转移模式。

解答:

最简单的例子:函数内部创建一个 String 并返回,调用者接管它:

1
2
3
4
5
6
7
8
fn make_name() -> String {
    String::from("Rust")
}

fn main() {
    let name = make_name();
    assert_eq!(name, "Rust");
}

你也可以把传进来的值吃进去、加工后再返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn add_suffix(mut s: String) -> String {
    s.push_str(" language");
    s
}

fn main() {
    let s = String::from("Rust");
    let s = add_suffix(s);
    assert_eq!(s, "Rust language");
}

如果只是想“借进来算一圈,再返回结果”,很多时候根本不该接管所有权,而应返回借用计算的结果或新值:

1
2
3
4
5
6
7
8
9
fn first_len(s: &str) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    assert_eq!(first_len(&s), 5);
    println!("{s}");
}

和“多返回值”结合时,Rust 很常用“把所有权一起带出来”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn trim_and_len(mut s: String) -> (String, usize) {
    s.truncate(s.trim_end().len());
    let len = s.len();
    (s, len)
}

fn main() {
    let (s, len) = trim_and_len(String::from("abc   "));
    assert_eq!(s, "abc");
    assert_eq!(len, 3);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func addSuffix(s string) string {
	return s + " language"
}

func main() {
	s := "Rust"
	s = addSuffix(s)
	fmt.Println(s)
}
  • Go 怎么做:返回值不会影响调用方原变量的可用性,因为不存在所有权失效这层语义。
  • Rust 为什么不同:Rust 把“谁拥有返回出来的资源”说清楚了,所以返回就是把所有权正式交给外层绑定。
  • Go 程序员易踩的坑:会把“传入并返回”误解为多余;在 Rust 里,这往往是在显式交接资源的责任。

记忆点:

  • 返回值通常意味着所有权转移给调用方。
  • “按值接收,再按值返回”是合法且常见的所有权流动。
  • 若函数不该拥有它,就别用按值参数。

Q7. 参数到底该写 String、&String 还是 &str?

Tags: hot beginner string 适用版本: Rust 1.0+

一句话答案:

默认优先写 &str:它最通用,既能接收字符串字面量,也能接收 String 的借用;只有函数必须拿走所有权或要修改并保留内容时,才写 String。

解答:

先看推荐写法:只读文本参数几乎总是 &str。

1
2
3
4
5
6
7
8
9
fn shout(s: &str) -> String {
    format!("{}!", s.to_uppercase())
}

fn main() {
    let owned = String::from("rust");
    assert_eq!(shout(&owned), "RUST!");
    assert_eq!(shout("go"), "GO!");
}

如果你写成 &String,它就只能接收 String 的借用,反而更窄:

1
2
3
4
5
6
7
8
9
fn len_of_string(s: &String) -> usize {
    s.len()
}

fn main() {
    let owned = String::from("rust");
    assert_eq!(len_of_string(&owned), 4);
    // 字面量 "go" 不能直接传给 &String
}

「❌ 错误写法」——把只读函数写成按值接收 String,调用完原值就没了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn print_len(s: String) {
    println!("{}", s.len());
}

fn main() {
    let name = String::from("rust");
    print_len(name);
    // println!("{name}");
    // error[E0382]: borrow of moved value: `name`
}

只有在函数要接管并保留或就地修改后返回时,才该收 String:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn append_world(mut s: String) -> String {
    s.push_str(" world");
    s
}

fn main() {
    let s = String::from("hello");
    let s = append_world(s);
    assert_eq!(s, "hello world");
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func shout(s string) string {
	return s + "!"
}

func main() {
	fmt.Println(shout("rust"))
}
  • Go 怎么做:字符串参数几乎总写 string,因为它本身就是只读字符串头,复制成本可控,也不涉及所有权失效。
  • Rust 为什么不同:String 是拥有堆内存的可增长字符串,&str 是对 UTF-8 文本的借用视图;写 &str 能把接口做得更宽。
  • Go 程序员易踩的坑:见到“读字符串”就写 String,结果把调用方值白白 move 走了。

记忆点:

  • 只读文本参数:优先 &str。
  • 很少需要 &String,它通常比 &str 更差。
  • 需要拥有或改完再交回:才用 String。

Q8. 泛型函数怎么写?::<T> 这个 turbofish 到底在干嘛?

Tags: hot beginner generics 适用版本: Rust 1.0+

一句话答案:

泛型函数用 fn name<T>(...) 定义,表示“这份逻辑可用于多种类型”;::<T> 叫 turbofish,是在编译器推断不出类型参数时,手动把类型写出来。

解答:

最基础的泛型函数长这样:

1
2
3
4
5
6
7
8
fn first<T>(xs: &[T]) -> Option<&T> {
    xs.first()
}

fn main() {
    assert_eq!(first(&[1, 2, 3]), Some(&1));
    assert_eq!(first(&["a", "b"]), Some(&"a"));
}

如果类型参数还需要满足某种能力,就加 trait 约束。这里的 trait 可以先理解成“接口 / 能力集合”:

1
2
3
4
5
6
7
8
fn show<T: std::fmt::Display>(value: T) {
    println!("{value}");
}

fn main() {
    show(42);
    show("rust");
}

当推断信息不足时,需要 turbofish:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn parse_num<T: std::str::FromStr>(s: &str) -> Result<T, T::Err> {
    s.parse::<T>()
}

fn main() {
    let a: i32 = parse_num("42").unwrap();
    let b = parse_num::<u64>("99").unwrap();
    assert_eq!(a, 42);
    assert_eq!(b, 99);
}

「❌ 错误写法」——编译器不知道你想解析成什么:

1
2
3
4
5
fn main() {
    // let n = "42".parse().unwrap();
    // error[E0284]: type annotations needed
    //  cannot satisfy `<_ as FromStr>::Err == _`
}

泛型不是运行时“装箱成万能盒子”;Rust 常见做法是 单态化(monomorphization,编译器按实际类型生成多份具体代码),因此通常没有动态分发成本。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func first[T any](xs []T) T {
	return xs[0]
}

func main() {
	fmt.Println(first([]int{1, 2, 3}))
	fmt.Println(first([]string{"a", "b"}))
}
  • Go 怎么做:Go 1.18+ 也有泛型,写法是 func f[T any](...)。
  • Rust 为什么不同:Rust 的 trait 约束更深地嵌进类型系统,很多“这个类型会不会实现某个方法”在编译期就被展开并检查。
  • Go 程序员易踩的坑:一看到 ::<T> 就紧张;它只是“这里我来明确告诉编译器类型参数是什么”。

记忆点:

  • 泛型函数的核心是 fn f<T>(...).
  • 有能力要求就写 T: Trait.
  • 推断不出类型时,用 turbofish ::<T> 补信息。

Q9. fn 指针、函数项、闭包到底有什么差别?为什么有的闭包传不进去?

Tags: hot beginner closure fn-pointer 适用版本: Rust 1.0+

一句话答案:

命名函数本身先是函数项(function item,某个具体函数自己的唯一类型),可自动退化成 fn(...) -> ... 函数指针;而闭包能捕获环境,只有“不捕获任何外部变量”的闭包才能转成 fn 指针,捕获了环境的闭包必须用 Fn / FnMut / FnOnce 这类 trait 约束接收。

解答:

先看函数指针:它只表示“去调用一段函数代码”,不携带环境状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn double(x: i32) -> i32 {
    x * 2
}

fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn main() {
    assert_eq!(apply(double, 3), 6);
}

不捕获环境的闭包,也能变成 fn 指针:

1
2
3
4
5
6
7
8
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn main() {
    let add_one: fn(i32) -> i32 = |x| x + 1;
    assert_eq!(apply(add_one, 5), 6);
}

但一旦闭包捕获了外部变量,它就不再只是“函数地址”,而是“代码 + 捕获到的环境”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn apply<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(x)
}

fn main() {
    let factor = 10;
    let times = |x| x * factor; // 捕获了 factor
    assert_eq!(apply(times, 3), 30);
}

「❌ 错误写法」——把捕获环境的闭包当成 fn 指针传:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

fn main() {
    let factor = 10;
    let times = |x: i32| x * factor;
    // let _ = apply(times, 3);
    // error[E0308]: mismatched types
    //  expected fn pointer, found closure
    //  note: closures can only be coerced to `fn` types if they do not capture any variables
}

如果你只是要“能调用就行”,大多数时候写泛型 F: Fn(...) -> ... 比写 fn(...) -> ... 更灵活。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func double(x int) int {
	return x * 2
}

func apply(f func(int) int, x int) int {
	return f(x)
}

func main() {
	factor := 10
	times := func(x int) int { return x * factor }
	fmt.Println(apply(double, 3))
	fmt.Println(apply(times, 3))
}
  • Go 怎么做:命名函数和闭包都统一落到 func(...) ... 这个函数类型里。
  • Rust 为什么不同:Rust 区分“不带环境的函数代码”和“带捕获环境的匿名对象”,这样能更精确地表达零成本调用和捕获语义。
  • Go 程序员易踩的坑:会以为 fn(i32) -> i32 就等于“任何可调用对象”;其实它只接真正的函数或不捕获环境的闭包。

记忆点:

  • fn(...) -> ... 只适合不捕获环境的可调用值。
  • 想接收闭包,优先写 F: Fn(...) -> ...。
  • 命名函数可自动转成函数指针;捕获环境的闭包不行。

Q10. impl Trait 放在参数和返回值位置,各代表什么?

Tags: common impl-trait 适用版本: 参数位置 impl Trait、返回位置 impl Trait 已稳定

一句话答案:

参数位置的 impl Trait 表示“来什么具体类型都行,只要实现了这个 trait”;返回位置的 impl Trait 表示“我返回某个具体类型,但我不想把它的真实名字暴露给调用者”。

解答:

参数位置常用于把签名写得更短:

1
2
3
4
5
6
7
8
9
fn print_twice(value: impl std::fmt::Display) {
    println!("{value}");
    println!("{value}");
}

fn main() {
    print_twice(42);
    print_twice("rust");
}

这和显式泛型本质相近:

1
2
3
4
5
6
7
8
fn print_twice<T: std::fmt::Display>(value: T) {
    println!("{value}");
    println!("{value}");
}

fn main() {
    print_twice(42);
}

返回位置的 impl Trait 则常用于隐藏很长、很丑或你不想公开承诺的具体类型:

1
2
3
4
5
6
7
8
fn make_iter() -> impl Iterator<Item = i32> {
    0..3
}

fn main() {
    let collected: Vec<_> = make_iter().collect();
    assert_eq!(collected, vec![0, 1, 2]);
}

一个重要限制:返回位置的 impl Trait 仍然只能对应“一个确定的具体类型”。根因和下面这个 if 表达式一样:分支若落成不同具体类型,编译器就会给出 E0308。

1
2
3
4
5
6
fn main() {
    // let value: i32 = if true { 1 } else { "x" };
    // error[E0308]: `if` and `else` have incompatible types
    let value: i32 = if true { 1 } else { 0 };
    let _ = value;
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type shower interface {
	String() string
}

type word string

func (w word) String() string { return string(w) }

func printTwice(v fmt.Stringer) {
	fmt.Println(v.String())
	fmt.Println(v.String())
}

func main() {
	printTwice(word("rust"))
}
  • Go 怎么做:参数最像“接收接口”;返回值若想隐藏具体类型,也常直接返回接口。
  • Rust 为什么不同:参数位置 impl Trait 通常仍是静态分发;返回位置 impl Trait 则是在“不暴露具体类型”的同时保持静态分发。
  • Go 程序员易踩的坑:把返回 impl Trait 误会成“可以随机返回任何实现了它的类型”;其实不行,底层必须是同一种具体类型。

记忆点:

  • 参数位置 impl Trait 约等于“简写泛型”。
  • 返回位置 impl Trait = 隐藏具体类型,但仍是一个确定类型。
  • 多分支返回不同具体类型时,不能直接用返回位置 impl Trait。

Q11. 什么时候该用关联函数,什么时候该用方法?self 三种写法又是什么意思?

Tags: common method 适用版本: Rust 1.0+

一句话答案:

写在 impl 里的无 self 函数叫关联函数(associated function),常用作构造器;带 self 的叫方法,&self 表示只读借用,&mut self 表示可变借用,self 表示把整个值吃掉。

解答:

先看三种最常见接收者:

 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
struct Counter {
    value: i32,
}

impl Counter {
    fn new(value: i32) -> Self {
        Self { value }
    }

    fn get(&self) -> i32 {
        self.value
    }

    fn inc(&mut self) {
        self.value += 1;
    }

    fn into_inner(self) -> i32 {
        self.value
    }
}

fn main() {
    let mut c = Counter::new(1);
    assert_eq!(c.get(), 1);
    c.inc();
    assert_eq!(c.into_inner(), 2);
}

如果方法只需要读值,优先 &self;需要改值时用 &mut self;值在此调用后不该再存在时,用 self:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Buffer {
    data: String,
}

impl Buffer {
    fn len(&self) -> usize {
        self.data.len()
    }

    fn push_x(&mut self) {
        self.data.push('x');
    }
}

fn main() {
    let mut b = Buffer {
        data: String::from("ab"),
    };
    assert_eq!(b.len(), 2);
    b.push_x();
    assert_eq!(b.data, "abx");
}

Go 对比:

 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
package main

import "fmt"

type Counter struct {
	Value int
}

func NewCounter(v int) Counter {
	return Counter{Value: v}
}

func (c Counter) Get() int {
	return c.Value
}

func (c *Counter) Inc() {
	c.Value++
}

func main() {
	c := NewCounter(1)
	fmt.Println(c.Get())
	c.Inc()
	fmt.Println(c.Value)
}
  • Go 怎么做:通过值接收者 / 指针接收者区分是否拷贝与是否可改。
  • Rust 为什么不同:Rust 还要精确表达借用和所有权转移,所以 self / &self / &mut self 三者语义更强。
  • Go 程序员易踩的坑:把 self 当成“只是值接收者”;在 Rust 里它意味着“这个方法会拿走整个对象”。

记忆点:

  • Type::new(...) 常是关联函数。
  • &self 读,&mut self 改,self 吃掉。
  • 方法接收者不只是调用语法糖,还编码了借用/所有权规则。

Q12. Rust 有函数重载、默认参数、命名参数吗?没有的话怎么替代?

Tags: common api-design 适用版本: Rust 1.0+

一句话答案:

Rust 没有 C++/Java 那种同名函数重载,也没有默认参数和命名参数;常见替代是不同函数名、配置结构体、Option 参数、Builder 模式或泛型 + trait。

解答:

同名重载在 Rust 里不成立:

1
2
3
4
5
6
7
fn greet(name: &str) {
    println!("hello, {name}");
}

fn main() {
    greet("Rust");
}

可选参数通常写 Option<T>:

1
2
3
4
5
6
7
8
9
fn connect(host: &str, port: Option<u16>) -> String {
    let port = port.unwrap_or(443);
    format!("{host}:{port}")
}

fn main() {
    assert_eq!(connect("example.com", None), "example.com:443");
    assert_eq!(connect("example.com", Some(8080)), "example.com:8080");
}

参数一多时,配置结构体更清楚:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct OpenOptions {
    read: bool,
    write: bool,
}

fn open(path: &str, opt: OpenOptions) -> String {
    format!("{path} {} {}", opt.read, opt.write)
}

fn main() {
    let msg = open(
        "demo.txt",
        OpenOptions {
            read: true,
            write: false,
        },
    );
    assert_eq!(msg, "demo.txt true false");
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

type OpenOptions struct {
	Read  bool
	Write bool
}

func open(path string, opt OpenOptions) string {
	return fmt.Sprintf("%s %v %v", path, opt.Read, opt.Write)
}

func main() {
	fmt.Println(open("demo.txt", OpenOptions{Read: true, Write: false}))
}
  • Go 怎么做:Go 也没有函数重载和默认参数,常用配置结构体与不同函数名。
  • Rust 为什么不同:Rust 更倾向让 API 形状显式、类型驱动,而不是靠语法糖隐藏默认行为。
  • Go 程序员易踩的坑:会下意识找“能不能多写一个同名版本”;Rust 不支持,直接换设计。

记忆点:

  • Rust 没有函数重载、默认参数、命名参数。
  • 可选参数:Option<T>。
  • 参数复杂:配置结构体 / Builder。

Q13. 可以在函数里面再定义函数吗?和闭包怎么选?

Tags: common nested-fn 适用版本: Rust 1.0+

一句话答案:

可以定义嵌套函数,但它不能捕获外层局部变量;想用到外层环境,就必须改用闭包。

解答:

嵌套函数适合“纯辅助逻辑,不依赖外层状态”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn outer(x: i32) -> i32 {
    fn double(y: i32) -> i32 {
        y * 2
    }

    double(x) + 1
}

fn main() {
    assert_eq!(outer(3), 7);
}

如果要捕获外层变量,闭包才行:

1
2
3
4
5
6
7
8
fn outer(x: i32) -> i32 {
    let add_x = |y: i32| y + x;
    add_x(10)
}

fn main() {
    assert_eq!(outer(5), 15);
}

「❌ 错误写法」——试图让嵌套 fn 直接用外层局部变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn outer(x: i32) -> i32 {
    fn inner(y: i32) -> i32 {
        // 取消下一行注释会编译失败:嵌套 fn 不能捕获外层的 x
        // y + x
        y
    }
    inner(1) + x
}

fn main() {
    assert_eq!(outer(5), 6);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func outer(x int) int {
	inner := func(y int) int {
		return y + x
	}
	return inner(1)
}

func main() {
	fmt.Println(outer(5))
}
  • Go 怎么做:Go 没有“命名嵌套函数”语法,但匿名函数天然可以捕获外层变量。
  • Rust 为什么不同:Rust 的嵌套 fn 仍然是普通函数项,不带环境;只有闭包才是“代码 + 环境”。
  • Go 程序员易踩的坑:看到嵌套 fn 就以为它和闭包一样能捕获 x;其实不能。

记忆点:

  • 嵌套 fn 不捕获环境。
  • 需要外层局部变量就用闭包。
  • “嵌套函数”和“闭包”在 Rust 里不是一个东西。

Q14. main 为什么可以返回 Result?这样和 ? 怎么配合?

Tags: common main question-mark 适用版本: main -> Result 已稳定

一句话答案:

main 可以写成 fn main() -> Result<(), E>,这样函数体里就能直接用 ?;一旦出错,程序会以非零退出码结束并打印错误。

解答:

最常见的 CLI 小程序写法如下:

1
2
3
4
5
6
7
8
use std::error::Error;
use std::fs;

fn main() -> Result<(), Box<dyn Error>> {
    let text = fs::read_to_string("Cargo.toml")?;
    println!("{}", text.len());
    Ok(())
}

如果你不想让 main 返回 Result,那就得自己 match:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::fs;

fn main() {
    match fs::read_to_string("Cargo.toml") {
        Ok(text) => println!("{}", text.len()),
        Err(err) => {
            eprintln!("read failed: {err}");
            std::process::exit(1);
        }
    }
}

「❌ 错误写法」——main 还是 (),却直接用 ?:

1
2
3
4
fn main() {
    // let text = std::fs::read_to_string("Cargo.toml")?;
    // error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
	"os"
)

func main() {
	data, err := os.ReadFile("go.mod")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println(len(data))
}
  • Go 怎么做:main 不能返回错误,只能手动 if err != nil 再 os.Exit(1)。
  • Rust 为什么不同:Rust 把 main 也纳入统一的“返回一个结果值”模型,于是 ? 可以一路用到程序入口。
  • Go 程序员易踩的坑:忘记改 main 返回类型,结果一上来就撞 E0277。

记忆点:

  • CLI/示例程序里,main -> Result<(), E> 很常见。
  • 想用 ?,返回类型必须支持它。
  • 需要自定义错误展示和退出码时,再手写 match。

Q15. const fn 是不是“只能在常量里用”的特殊函数?

Tags: common const-fn 适用版本: Rust 1.0+,可用能力随版本逐步扩展

一句话答案:

不是。const fn 在运行时就是普通函数;它只是额外承诺“这段函数在编译期常量上下文里也能求值”。

解答:

先看最基本用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const fn max(a: i32, b: i32) -> i32 {
    if a > b {
        a
    } else {
        b
    }
}

const LIMIT: i32 = max(10, 20);

fn main() {
    assert_eq!(LIMIT, 20);
}

同一个 const fn 也能在运行时当普通函数调用:

1
2
3
4
5
6
7
8
const fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let x = add(3, 4);
    assert_eq!(x, 7);
}

如果函数体里用了当前稳定版还不允许出现在 const 上下文的操作,它就不能标成 const fn;这类限制是“编译期可求值能力”的限制,不是普通调用能力的限制。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

const Limit = 20

func add(a, b int) int {
	return a + b
}

func main() {
	fmt.Println(Limit, add(3, 4))
}
  • Go 怎么做:Go 有 const,但没有“可在编译期执行的函数”这层语法。
  • Rust 为什么不同:Rust 把一部分可纯计算的函数扩展到编译期执行,既复用代码,又让常量定义更灵活。
  • Go 程序员易踩的坑:把 const fn 误以为“特殊且只能给常量用”;其实运行时照样能调。

记忆点:

  • const fn 也是普通函数。
  • 它额外支持在常量上下文求值。
  • 能不能标 const,取决于函数体是否满足当前 stable 的 const 限制。

Q16. Rust 有可变参数函数吗?能不能写成 Go 那样的 ...T?

Tags: occasional variadic 适用版本: 安全 Rust 无通用 variadic

一句话答案:

安全 Rust 没有 Go 那种 ...T 形式的普通可变参数函数;常见替代是切片、迭代器、数组、元组或宏。

解答:

最常见的替代是切片:

1
2
3
4
5
6
7
fn sum(xs: &[i32]) -> i32 {
    xs.iter().sum()
}

fn main() {
    assert_eq!(sum(&[1, 2, 3]), 6);
}

或者更泛化地接收“任何可迭代输入”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn sum<I>(xs: I) -> i32
where
    I: IntoIterator<Item = i32>,
{
    xs.into_iter().sum()
}

fn main() {
    assert_eq!(sum(vec![1, 2, 3]), 6);
    assert_eq!(sum([4, 5]), 9);
}

要做“像 println! 那样参数个数不固定”的效果,Rust 更常见的武器其实是宏,而不是函数。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func sum(xs ...int) int {
	total := 0
	for _, x := range xs {
		total += x
	}
	return total
}

func main() {
	fmt.Println(sum(1, 2, 3))
}
  • Go 怎么做:直接用 ...T 定义 variadic 函数。
  • Rust 为什么不同:Rust 更倾向把“变长输入”表示为集合/迭代器,把“语法可变形”留给宏系统。
  • Go 程序员易踩的坑:会到处寻找 fn f(xs: ...i32);稳定 Rust 没这个语法。

记忆点:

  • 变长输入:优先 &[T] 或 IntoIterator.
  • 想要 println! 那种调用体验,通常需要宏。
  • 安全 Rust 日常业务代码里没有通用 variadic 函数。

Q17. Rust 支持递归函数吗?需要特别担心什么?

Tags: occasional recursion 适用版本: Rust 1.0+

一句话答案:

支持递归,但要和 Go 一样警惕栈深;Rust 不保证尾递归优化,所以深递归更稳妥的做法通常还是改写成循环。

解答:

普通递归当然可以写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn fact(n: u64) -> u64 {
    if n <= 1 {
        1
    } else {
        n * fact(n - 1)
    }
}

fn main() {
    assert_eq!(fact(5), 120);
}

但深递归可能直接把栈打爆,更稳妥的常常是迭代:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn fact_iter(n: u64) -> u64 {
    let mut acc = 1;
    for i in 2..=n {
        acc *= i;
    }
    acc
}

fn main() {
    assert_eq!(fact_iter(5), 120);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func fact(n uint64) uint64 {
	if n <= 1 {
		return 1
	}
	return n * fact(n-1)
}

func main() {
	fmt.Println(fact(5))
}
  • Go 怎么做:也支持递归,也同样会受栈深影响。
  • Rust 为什么不同:在函数语义上并无特别不同,但 Rust 社区对“显式控制资源和栈行为”通常更敏感。
  • Go 程序员易踩的坑:以为编译器会自动把尾递归优化成循环;Rust 不保证。

记忆点:

  • 递归合法,但深递归要小心栈溢出。
  • Rust 不承诺尾递归优化。
  • 能写循环时,通常更稳。

Q18. -> ! 是什么?为什么 panic!() 能塞进任何返回类型里?

Tags: advanced never 适用版本: ! 在发散函数中的稳定用法已可依赖

一句话答案:

! 叫 never 类型,表示“这个函数永远不会正常返回”;因为它根本不会产出值,所以编译器允许把它放进任何“本来需要一个值”的位置。

解答:

最典型的发散函数是无限循环或直接 panic:

1
2
3
4
5
6
7
fn never_returns() -> ! {
    panic!("boom");
}

fn main() {
    let _f: fn() -> ! = never_returns;
}

这让 match 或 if 某个分支可以“没有正常值”,但整体仍然类型成立:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn get_name(flag: bool) -> &'static str {
    if flag {
        "rust"
    } else {
        panic!("missing name")
    }
}

fn main() {
    assert_eq!(get_name(true), "rust");
}

也可以用于“失败就退出程序”的工具函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn die(msg: &str) -> ! {
    eprintln!("{msg}");
    std::process::exit(1);
}

fn pick(opt: Option<i32>) -> i32 {
    match opt {
        Some(x) => x,
        None => die("missing number"),
    }
}

fn main() {
    assert_eq!(pick(Some(7)), 7);
}

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func pick(ok bool) string {
	if ok {
		return "rust"
	}
	panic("missing name")
}

func main() {
	fmt.Println(pick(true))
}
  • Go 怎么做:可以 panic,但没有 ! 这种类型系统层面的“永不返回类型”。
  • Rust 为什么不同:Rust 把“控制流不会回来”也编码进类型系统,因此很多分支类型检查能更精确。
  • Go 程序员易踩的坑:看到 ! 以为是“布尔取反”或什么宏语法;在函数返回位置里,它就是一个真正的类型。

记忆点:

  • -> ! = 这个函数永不正常返回。
  • panic!、loop {}、process::exit 一类表达式都能体现 never 语义。
  • never 类型能和别的返回类型在分支里统一。

Q19. 返回参数里的引用时,生命周期该怎么写?

Tags: hot lifetime reference E0106 适用版本: Rust 1.0+

一句话答案:

返回的引用必须“借自某个输入参数(或 'static 数据)”,不能借自函数体内的局部变量;多个输入引用时,往往要显式标 'a,告诉编译器返回值到底跟谁活得一样久。

解答:

单个输入引用时,生命周期省略规则通常够用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let s = String::from("hello rust");
    assert_eq!(first_word(&s), "hello");
}

两个输入都可能成为返回来源时,省略会失败,必须命名:

1
2
3
4
5
// 这样写会报错(缺生命周期):
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }
// error[E0106]: missing lifetime specifier
1
2
3
4
5
6
7
8
9
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let a = String::from("abcd");
    let b = String::from("xyz");
    assert_eq!(longest(&a, &b), "abcd");
}

「❌ 错误写法」——返回局部变量引用(悬垂):

1
2
3
4
5
// fn bad() -> &str {
//     let s = String::from("nope");
//     &s
// }
// error[E0515]: cannot return reference to local variable `s`
1
2
3
4
5
fn main() {
    let owned = String::from("ok");
    let r: &str = &owned; // 调用方拥有数据时才合法
    println!("{r}");
}

需要把“新造出来的数据”交出去时,返回 String(所有权),不要返回 &str。

Go 对比:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func longest(x, y string) string {
	if len(x) > len(y) {
		return x
	}
	return y
}

func main() {
	fmt.Println(longest("abcd", "xyz"))
}
  • Go 怎么做:string 头拷贝很便宜,返回局部 string 也安全(底层数据由 GC 管)。
  • Rust 为什么不同:&str 不拥有缓冲,签名必须证明它不会比源数据活得更久。
  • Go 程序员易踩的坑:按 Go 习惯返回“函数里临时拼的字符串的引用”;在 Rust 里应返回 String。

记忆点:

  • 返回引用 = 必须挂在某个输入(或 'static)上。
  • 多输入候选来源 → 写 'a。
  • 新数据用拥有型返回值,别硬返回 &。

Q20. return、? 和尾表达式混用时要注意什么?

Tags: common return ? Result expression 适用版本: Rust 1.0+

一句话答案:

? 是“失败就提前返回”的表达式;成功路径常靠最后一个无分号表达式(或 Ok(...))收尾。最常见的坑是:前面用了 ?,最后却直接返回裸值而忘了包进 Ok,或尾表达式多写了分号变成 ()。

解答:

标准混用:中间 ?,末尾 Ok(也可写成 Ok 包住尾表达式):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn parse_pair(s: &str) -> Result<(i32, i32), std::num::ParseIntError> {
    let mut parts = s.split(',');
    let a = parts.next().unwrap_or("").parse()?;
    let b = parts.next().unwrap_or("").parse()?;
    Ok((a, b)) // 成功路径必须是 Result
}

fn main() {
    assert_eq!(parse_pair("3,4").unwrap(), (3, 4));
}

「❌ 错误写法」——? 之后尾表达式是元组,类型对不上:

1
2
3
4
5
6
7
8
9
// fn parse_pair(s: &str) -> Result<(i32, i32), std::num::ParseIntError> {
//     let parts: Vec<_> = s.split(',').collect();
//     let a = parts[0].parse()?;
//     let b = parts[1].parse()?;
//     (a, b) // 少了 Ok(...)
// }
// error[E0308]: mismatched types
//   expected enum `Result<(i32, i32), ParseIntError>`
//     found tuple `(_, _)`

return 适合早退;与尾表达式并存时注意别写“死代码”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn pick(flag: bool) -> Result<&'static str, &'static str> {
    if !flag {
        return Err("no"); // 提前返回
    }
    Ok("yes") // 尾表达式
}

fn main() {
    assert_eq!(pick(true).unwrap(), "yes");
    assert!(pick(false).is_err());
}

分号规则(见 Q2)在 Result 函数里同样致命:最后一行 Ok(x); 会让函数实际返回 ()。

Go 对比:

 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
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func parsePair(s string) (int, int, error) {
	parts := strings.Split(s, ",")
	a, err := strconv.Atoi(parts[0])
	if err != nil {
		return 0, 0, err
	}
	b, err := strconv.Atoi(parts[1])
	if err != nil {
		return 0, 0, err
	}
	return a, b, nil
}

func main() {
	a, b, err := parsePair("3,4")
	fmt.Println(a, b, err)
}
  • Go 怎么做:每个错误分支手写 if err != nil { return ... },成功也显式 return。
  • Rust 为什么不同:? 压缩失败路径;成功路径常靠尾表达式/Ok 一次写清。
  • Go 程序员易踩的坑:以为“解析完直接 (a,b) 就行”,忘了外层还是 Result。

记忆点:

  • ? 失败早退;成功路径要落到 Ok(...)(或兼容类型)。
  • 尾表达式不要多余分号。
  • return 管早退,别和“最后一行产值”打架。

最后修改 August 11, 2026: 更新 (70a5af133)