16-结构体

面向 Go 程序员讲清结构体定义、方法、更新语法与所有权影响

结构体 (Structs)

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

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

本篇能解决什么:

  • 你会不会把 Rust struct 当成“没有 GC 的 Go struct”,结果在 ..base、方法接收者上连环报错?
  • 你是否分不清 self / &self / &mut self,以及关联函数和方法的差别?
  • 你会不会在字段默认私有、derive、newtype、带生命周期字段这些题上反复踩坑?
  • 你是否想知道:什么时候该 Builder、什么时候该拥有 String、什么时候该借 &str?

术语速查表:

术语 / 缩略词全称 / 读法中文一句话解释Go 里的近亲
struct—结构体把相关字段聚合成一个类型struct
field—字段结构体里的命名成员结构体字段
implimplementation实现块给类型挂方法和关联函数的地方方法集合(无独立语法)
receiver—方法接收者方法第一个参数:self / &self / &mut self值/指针接收者
associated function—关联函数impl 里没有 self 的函数,常用 Type::new包级构造函数近亲
update syntax—更新语法User { active: false, ..base } 从 base 补齐其余字段无直接语法糖
partial move—部分移动只移走结构体里部分非 Copy 字段Go 无对应
newtype—新类型包装单字段元组结构体,用来区分语义自定义命名类型近亲
ZSTZero-Sized Type零大小类型编译期大小为 0 的类型(如单元结构体)无直接对应
RAIIResource Acquisition Is Initialization资源获取即初始化资源跟着值的生命周期自动释放常靠 defer
GCGarbage Collector垃圾回收器运行时回收不用对象Go 默认机制
lifetime—生命周期引用有效存活区间的编译期标注无显式对应

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

热度索引:

热度题目
hotQ1, Q2, Q3, Q4, Q17
commonQ5, Q6, Q7, Q8, Q9, Q10, Q11, Q18, Q19
occasionalQ12, Q13, Q14
advancedQ15, Q16

Q1. Rust 结构体和 Go struct 最像的地方是什么?

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

一句话答案:

都是“把相关字段捆成一个类型”;差别在于 Rust 还把所有权、可见性和方法接收者一起编进这个类型里。

解答:

Go 的 struct 主要是字段聚合;Rust 的 struct 同样聚合字段,但字段类型一旦带上 String、Vec 等拥有型数据,赋值、传参、..base 更新都会触发 move 规则。先把三种形式认全:命名字段结构体、元组结构体、单元结构体(ZST,Zero-Sized Type,零大小类型)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct User {
    name: String,
    active: bool,
}

struct Point(i32, i32);

struct Marker;

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    let p = Point(1, 2);
    let _m = Marker;
    println!("{} {} {}", u.name, p.0, u.active);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct User {
    name: String,
    active: bool,
}

fn build(name: String, active: bool) -> User {
    User { name, active } // 字段初始化简写:变量名与字段名相同
}

fn main() {
    let u = build(String::from("Ada"), true);
    println!("{} {}", u.name, u.active);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[derive(Debug)]
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    println!("{u:?}");
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u := User{Name: "Ada", Active: true}
	fmt.Println(u.Name, u.Active)
}
  • Go 怎么做:struct 字段聚合 + 方法,值复制很常见。
  • Rust 为什么不同:类型还要回答“谁拥有堆数据、谁能改、谁能从外部构造”。
  • Go 程序员易踩的坑:把带 String 的 struct 当成随手复制的小值。

Q2. 为什么字段里有 String 就会牵出所有权问题?

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

一句话答案:

String 拥有堆上的 UTF-8 字节;结构体拥有这个 String,所以结构体赋值/传参默认是 move,不是“便宜拷贝一份”。

解答:

Go 里 string 头复制很便宜,原变量通常还能用。Rust 里 String 不是 Copy:把整个 User 赋给别人,等于把 name 的所有权也交出去。只想临时看一眼时,借 &User 或 &str。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = u1;
    println!("{} {}", u2.name, u2.active);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = u1;
    println!("{}", u1.name);
    // error[E0382]: borrow of moved value: `u1`
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct User {
    name: String,
    active: bool,
}

fn show(u: &User) {
    println!("{} {}", u.name, u.active);
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    show(&u);
    println!("{}", u.name); // 仍可用:只是借出去看过
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u1 := User{Name: "Ada", Active: true}
	u2 := u1
	fmt.Println(u1.Name, u2.Name)
}
  • Go 怎么做:赋值通常复制 struct 值(含 string 头)。
  • Rust 为什么不同:非 Copy 字段让整个结构体默认走 move,避免双释放。
  • Go 程序员易踩的坑:赋值后继续用旧变量,撞上 E0382。

Q3. 结构体更新语法 ..base 到底做了什么?

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

一句话答案:

User { active: false, ..base } 用你写明的字段覆盖,其余字段从 base 搬过来;非 Copy 字段会被 move,这叫 partial move(部分移动)。

解答:

..base 必须写在结构体字面量最后。它不是“浅拷贝一份再改”,而是:未写出的字段按所有权规则从 base 取走。bool/i32 等 Copy 字段取走后原值仍可读;String 取走后原字段失效,整个 base 也不能再按值使用。要两边都留着,就 ..base.clone()(需 #[derive(Clone)],写在该 struct 定义正上方)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = User {
        active: false,
        ..u1
    };
    println!("{} {}", u2.name, u2.active);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = User {
        active: false,
        ..u1
    };
    println!("{}", u1.name);
    // error[E0382]: borrow of moved value: `u1.name`
    println!("{}", u2.active);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Clone)]
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = User {
        active: false,
        ..u1.clone()
    };
    println!("{} {}", u1.name, u2.active);
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u1 := User{Name: "Ada", Active: true}
	u2 := u1
	u2.Active = false
	fmt.Println(u1.Name, u1.Active, u2.Active)
}
  • Go 怎么做:先整份赋值,再改字段;两边都还在。
  • Rust 为什么不同:更新语法默认搬所有权,避免悄悄复制堆数据。
  • Go 程序员易踩的坑:写完 ..u1 还以为 u1.name 能继续用。

Q4. 方法接收者 self / &self / &mut self 怎么选?

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

一句话答案:

只读用 &self,要改字段用 &mut self,要消费/转换所有权才用 self。

解答:

这对应 Go 的值接收者 / 指针接收者,但 Rust 更严:&self 不能改;self 会把调用方手里的值吃掉。日常 API 默认 &self / &mut self;构建器收尾、into_* 转换再用 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
struct Rect {
    w: u32,
    h: u32,
}

impl Rect {
    fn area(&self) -> u32 {
        self.w * self.h
    }

    fn grow(&mut self, dw: u32) {
        self.w += dw;
    }

    fn into_area(self) -> u32 {
        self.w * self.h
    }
}

fn main() {
    let mut r = Rect { w: 3, h: 4 };
    println!("{}", r.area());
    r.grow(1);
    println!("{}", r.into_area());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct Rect {
    w: u32,
    h: u32,
}

impl Rect {
    fn into_area(self) -> u32 {
        self.w * self.h
    }
}

fn main() {
    let r = Rect { w: 3, h: 4 };
    let a = r.into_area();
    println!("{}", r.w);
    // error[E0382]: borrow of moved value: `r`
    println!("{a}");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Counter {
    n: i32,
}

impl Counter {
    fn bump(&mut self) {
        self.n += 1;
    }
}

fn main() {
    let mut c = Counter { n: 0 };
    c.bump();
    println!("{}", c.n);
}

Go 对比:

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

import "fmt"

type Rect struct{ W, H uint }

func (r Rect) Area() uint { return r.W * r.H }

func (r *Rect) Grow(dw uint) { r.W += dw }

func main() {
	r := Rect{W: 3, H: 4}
	fmt.Println(r.Area())
	r.Grow(1)
	fmt.Println(r.W)
}
  • Go 怎么做:值/指针接收者都能写,编译器较少阻止“用完还用”。
  • Rust 为什么不同:接收者签名直接编码借用或消费。
  • Go 程序员易踩的坑:把该 &self 的查询写成 self,调用后原值没了。

Q5. 关联函数和方法差在哪?

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

一句话答案:

关联函数没有 self,用 Type::name(...) 调用;方法有接收者,用 value.name(...) 调用。

解答:

最常见的关联函数是构造器 new。Self 是当前类型别名,写在 impl Type 里等于写 Type。Go 没有完全同构的语法,通常是包级 NewXxx 或返回指针的工厂函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct Rect {
    w: u32,
    h: u32,
}

impl Rect {
    fn new(w: u32, h: u32) -> Self {
        Self { w, h }
    }

    fn area(&self) -> u32 {
        self.w * self.h
    }
}

fn main() {
    let r = Rect::new(3, 4);
    assert_eq!(r.area(), 12);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Rect {
    w: u32,
    h: u32,
}

impl Rect {
    fn square(side: u32) -> Self {
        Self { w: side, h: side }
    }
}

fn main() {
    let r = Rect::square(5);
    println!("{} {}", r.w, r.h);
}

Go 对比:

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

import "fmt"

type Rect struct{ W, H uint }

func NewRect(w, h uint) Rect { return Rect{W: w, H: h} }

func (r Rect) Area() uint { return r.W * r.H }

func main() {
	r := NewRect(3, 4)
	fmt.Println(r.Area())
}
  • Go 怎么做:工厂函数 + 方法分开写。
  • Rust 为什么不同:两者都进同一个 impl,但调用语法不同。
  • Go 程序员易踩的坑:对关联函数写 r.new(...),或对方法写 Rect::area(r) 却忘了接收者形式。

Q6. 什么时候该 derive(Debug)?

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

一句话答案:

几乎所有要 println!("{x:?}")、测日志、看断言失败信息的类型,都应在定义上方加 #[derive(Debug)]。

解答:

写在 struct / enum 定义正上方:#[derive(Debug)]。字段类型也必须实现 Debug,否则 derive 失败。Go 常用 %+v;Rust 没有默认万能打印,需要 Debug。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[derive(Debug)]
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    println!("{u:?}");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    println!("{u:?}");
    // error[E0277]: `User` doesn't implement `Debug`
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u := User{Name: "Ada", Active: true}
	fmt.Printf("%+v\n", u)
}
  • Go 怎么做:fmt 对结构体开箱即用。
  • Rust 为什么不同:调试打印是显式 trait,避免默认可打印敏感字段时无约束。
  • Go 程序员易踩的坑:一上来就 {u:?},却忘了 derive(Debug)。

Q7. 什么时候该 derive(Clone),什么时候不该?

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

一句话答案:

需要显式复制(例如 ..base.clone()、放入多处拥有者)时 derive;持有不可克隆资源(文件句柄、锁、唯一 ID)时不要盲目 derive。

解答:

Clone 是显式深/浅复制约定(对 String 是再分配一份堆数据)。写在类型定义上方:#[derive(Clone)]。若字段不能 Clone,要么手写 Clone,要么改设计。不要用 Clone 掩盖本该借用的 API。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#[derive(Clone, Debug)]
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = u1.clone();
    println!("{u1:?} {u2:?}");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Clone)]
struct User {
    name: String,
    active: bool,
}

fn main() {
    let u1 = User {
        name: String::from("Ada"),
        active: true,
    };
    let u2 = User {
        active: false,
        ..u1.clone()
    };
    println!("{} {}", u1.name, u2.active);
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u1 := User{Name: "Ada", Active: true}
	u2 := u1 // 值拷贝
	fmt.Println(u1, u2)
}
  • Go 怎么做:赋值常常已经拷贝了值头。
  • Rust 为什么不同:复制要显式,避免隐藏的堆分配。
  • Go 程序员易踩的坑:到处 .clone() 消错,却不改接收者/借用设计。

Q8. 元组结构体有什么用?

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

一句话答案:

给“有类型、但字段名不重要”的值起名,例如坐标点、像素颜色;也常作 newtype 的载体。

解答:

用 .0、.1 访问。比裸元组更可读,又能挂方法。字段很多、语义复杂时仍应用命名字段结构体。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Point(i32, i32);

impl Point {
    fn dist2(&self) -> i32 {
        self.0 * self.0 + self.1 * self.1
    }
}

fn main() {
    let p = Point(3, 4);
    println!("{} {}", p.0, p.dist2());
}
1
2
3
4
5
6
struct Color(u8, u8, u8);

fn main() {
    let c = Color(255, 128, 0);
    println!("rgb({},{},{})", c.0, c.1, c.2);
}

Go 对比:

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

import "fmt"

type Point struct{ X, Y int }

func main() {
	p := Point{X: 3, Y: 4}
	fmt.Println(p.X, p.Y)
}
  • Go 怎么做:几乎总是命名字段。
  • Rust 为什么不同:元组结构体在“轻量命名类型”场景更短。
  • Go 程序员易踩的坑:把元组结构体当匿名 tuple,忘记它已是独立类型、不能和 (i32,i32) 混用。

Q9. newtype 模式为什么常见?

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

一句话答案:

用单字段元组结构体包一层,让 UserId 和 i64、Meters 和 f64 在类型上不能混用,并单独实现行为。

解答:

写在业务模块的 types.rs 或同文件类型区:struct UserId(u64);。需要时再 impl UserId { ... }。这比 Go 的 type UserId int64 更强:Rust 不会自动当成底层类型用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct UserId(u64);

impl UserId {
    fn get(self) -> u64 {
        self.0
    }
}

fn main() {
    let id = UserId(42);
    println!("{}", id.get());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Meters(f64);
struct Seconds(f64);

fn speed(dist: Meters, time: Seconds) -> f64 {
    dist.0 / time.0
}

fn main() {
    let d = Meters(100.0);
    let t = Seconds(9.58);
    println!("{}", speed(d, t));
}

Go 对比:

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

import "fmt"

type UserId int64

func main() {
	var id UserId = 42
	fmt.Println(int64(id))
}
  • Go 怎么做:定义类型别名式命名类型,转换仍很常见。
  • Rust 为什么不同:newtype 是真正的新类型,默认不与底层类型互通。
  • Go 程序员易踩的坑:期望 UserId 能直接当 u64 做算术。

Q10. 字段默认私有意味着什么?

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

一句话答案:

即使 pub struct,字段默认仍私有;模块外不能直接写字面量构造,也不能读私有字段,除非字段标 pub 或你提供构造器/访问器。

解答:

在定义字段处写 pub name: String 才导出字段。更常见做法:结构体 pub,字段私有,在同模块 impl 里提供 pub fn new(...)。这比 Go 的首字母大小写导出更细:类型公开 ≠ 字段公开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
mod account {
    pub struct User {
        pub name: String,
        active: bool,
    }

    impl User {
        pub fn new(name: String, active: bool) -> Self {
            Self { name, active }
        }

        pub fn active(&self) -> bool {
            self.active
        }
    }
}

fn main() {
    let u = account::User::new(String::from("Ada"), true);
    println!("{} {}", u.name, u.active());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
mod account {
    pub struct User {
        pub name: String,
        active: bool,
    }
}

fn main() {
    let _u = account::User {
        name: String::from("Ada"),
        active: true,
    };
    // error[E0451]: field `active` of struct `User` is private
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	active bool // 包外不可见
}

func main() {
	u := User{Name: "Ada"}
	fmt.Println(u.Name)
}
  • Go 怎么做:靠标识符首字母控制导出。
  • Rust 为什么不同:pub 可标在类型和每个字段上,粒度更细。
  • Go 程序员易踩的坑:以为 pub struct 就能在外部用字段字面量随便拼。

Q11. 为什么结构体参数常优先借用而不是按值拿走?

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

一句话答案:

按值会 move(或大结构体按位复制);只读用 &T,要改用 &mut T,只有转移所有权时才接 T。

解答:

这和 Q4 同一套直觉。小 Copy 结构体按值传也可以;带堆数据或较大时,借用更省事,也让调用方继续拥有原值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct User {
    name: String,
    active: bool,
}

fn label(u: &User) -> String {
    format!("{}:{}", u.name, u.active)
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    println!("{}", label(&u));
    println!("{}", u.name);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct User {
    name: String,
    active: bool,
}

fn consume(u: User) -> String {
    u.name
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        active: true,
    };
    let name = consume(u);
    println!("{name}");
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func Label(u User) string {
	return fmt.Sprintf("%s:%v", u.Name, u.Active)
}

func main() {
	u := User{Name: "Ada", Active: true}
	fmt.Println(Label(u), u.Name)
}
  • Go 怎么做:值传递很常见,大结构体才改指针。
  • Rust 为什么不同:非 Copy 按值等于移交所有权。
  • Go 程序员易踩的坑:API 到处接 User,调用方被迫不断 clone。

Q12. 带生命周期的结构体什么时候值得写?

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

一句话答案:

结构体需要“借用别人的数据且不拥有它”时才写生命周期参数;能改成拥有 String/Vec 往往更简单。

解答:

写法:struct View<'a> { text: &'a str },'a 写在类型名后、字段引用上。含义是:View 不能比它借用的 str 活得更久。Go 靠 GC 持有底层数据;Rust 要你在类型上写清借用关系。初学优先拥有型字段。

1
2
3
4
5
6
7
8
9
struct View<'a> {
    text: &'a str,
}

fn main() {
    let s = String::from("hello");
    let v = View { text: &s };
    println!("{}", v.text);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct View<'a> {
    text: &'a str,
}

fn main() {
    let v;
    {
        let s = String::from("hello");
        v = View { text: &s };
    }
    println!("{}", v.text);
    // error[E0597]: `s` does not live long enough
}

Go 对比:

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

import "fmt"

type View struct{ Text string }

func main() {
	s := "hello"
	v := View{Text: s}
	fmt.Println(v.Text)
}
  • Go 怎么做:字符串数据由运行时管理,结构体里存 string 很轻松。
  • Rust 为什么不同:借用字段必须声明“我依赖谁还活着”。
  • Go 程序员易踩的坑:一上来就在 struct 里塞满 &str,被生命周期淹没。

Q13. 为什么自引用结构体不好做?

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

一句话答案:

字段不能安全地既拥有一块数据、又同时持有指向自己内部的引用;搬家/move 会让内部指针失效。

解答:

Go 里指针指自己很常见。Rust 里普通引用结构体在 move 后地址可能变,编译器拒绝这种自引用。常见替代:把数据放一起、用索引代替内部指针,或拆成“拥有缓冲 + 临时借用视图”两个类型。需要真正自引用时才考虑 Pin 等进阶工具(本篇不展开)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Owns {
    data: String,
}

fn main() {
    let o = Owns {
        data: String::from("hello"),
    };
    let view: &str = &o.data;
    println!("{view}");
}
1
2
3
4
5
// 签名示意,非完整程序:下面这种“结构体字段引用自己另一字段”的模式
struct Bad {
    data: String,
    // view: &str, // 想指向 data —— 生命周期与 move 语义都会卡住
}

Go 对比:

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

import "fmt"

type Node struct {
	Val  int
	Self *Node
}

func main() {
	n := &Node{Val: 1}
	n.Self = n
	fmt.Println(n.Self.Val)
}
  • Go 怎么做:结构体里存指向自己的指针很自然。
  • Rust 为什么不同:默认 move 可能改变地址,内部引用会悬空。
  • Go 程序员易踩的坑:把链表/图直接按 Go 指针思维平移过来。

Q14. Builder 模式在 Rust 里为什么常见?

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

一句话答案:

字段多、可选配置多、又想保持字段私有/一次构造完整时,用 Builder 逐步设置,最后 build() 产出拥有型值。

解答:

通常在同一文件或 foo/builder.rs 里写 FooBuilder,方法接 &mut self 或 self(链式),build(self) -> Foo。这比一长串构造参数清晰,也比外部随意改私有字段安全。

 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
34
35
36
37
38
39
40
struct Config {
    host: String,
    port: u16,
}

struct ConfigBuilder {
    host: String,
    port: u16,
}

impl ConfigBuilder {
    fn new() -> Self {
        Self {
            host: String::from("127.0.0.1"),
            port: 8080,
        }
    }

    fn host(mut self, host: impl Into<String>) -> Self {
        self.host = host.into();
        self
    }

    fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    fn build(self) -> Config {
        Config {
            host: self.host,
            port: self.port,
        }
    }
}

fn main() {
    let c = ConfigBuilder::new().host("localhost").port(3000).build();
    println!("{}:{}", c.host, c.port);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Config {
    host: String,
    port: u16,
}

impl Config {
    fn new(host: String, port: u16) -> Self {
        Self { host, port }
    }
}

fn main() {
    let c = Config::new(String::from("localhost"), 3000);
    println!("{}:{}", c.host, c.port);
}

Go 对比:

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

import "fmt"

type Config struct {
	Host string
	Port uint16
}

func main() {
	c := Config{Host: "localhost", Port: 3000}
	fmt.Println(c.Host, c.Port)
}
  • Go 怎么做:常用结构体字面量 + functional options。
  • Rust 为什么不同:字段私有 + 所有权让 Builder/new 更常见。
  • Go 程序员易踩的坑:对外暴露所有字段,失去不变量保护。

Q15. Go 的匿名字段直觉在 Rust 里能直接迁移吗?

Tags: advanced struct 适用版本: Rust 1.0+

一句话答案:

不能直接迁移:Rust 没有 Go 那种匿名字段提升;要组合就显式嵌套字段,要复用行为就用 trait 或手动委托方法。

解答:

Go 的嵌入会把内嵌类型的方法“提升”到外层。Rust 写 struct Dog { pet: Pet } 后,调用的是 dog.pet.speak(),除非你手写转发方法或实现相同 trait。这更啰嗦,但类型关系更清晰。

 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
struct Pet {
    name: String,
}

impl Pet {
    fn greet(&self) -> String {
        format!("hi {}", self.name)
    }
}

struct Dog {
    pet: Pet,
    breed: String,
}

impl Dog {
    fn greet(&self) -> String {
        self.pet.greet()
    }
}

fn main() {
    let d = Dog {
        pet: Pet {
            name: String::from("Rex"),
        },
        breed: String::from("corgi"),
    };
    println!("{} {}", d.greet(), d.breed);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Pet {
    name: String,
}

struct Dog {
    pet: Pet,
}

fn main() {
    let d = Dog {
        pet: Pet {
            name: String::from("Rex"),
        },
    };
    println!("{}", d.pet.name);
}

Go 对比:

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

import "fmt"

type Pet struct{ Name string }

func (p Pet) Greet() string { return "hi " + p.Name }

type Dog struct {
	Pet
	Breed string
}

func main() {
	d := Dog{Pet: Pet{Name: "Rex"}, Breed: "corgi"}
	fmt.Println(d.Greet(), d.Name, d.Breed)
}
  • Go 怎么做:匿名字段自动提升字段/方法。
  • Rust 为什么不同:偏好显式字段访问与 trait,避免隐式提升。
  • Go 程序员易踩的坑:以为嵌套 struct 会自动拥有内层方法。

Q16. 本章最实用的结构体设计准则是什么?

Tags: advanced struct 适用版本: Rust 1.0+

一句话答案:

字段默认私有 + 构造器约束不变量;能借用就别拿走;需要复制再 Clone;更新语法想保留原值就 clone;别一上来自引用和复杂生命周期。

解答:

落地检查清单:

  1. 字段类型先选拥有型(String/Vec),确认 API 稳定后再考虑借用字段。
  2. 方法接收者默认 &self/&mut self。
  3. ..base 后假设非 Copy 字段已 move。
  4. 导出类型时优先 new/Builder,而不是把所有字段 pub。
  5. Debug 常开;Clone/Copy 按语义开,不按“好写”开。
 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
#[derive(Debug, Clone)]
struct User {
    name: String,
    active: bool,
}

impl User {
    fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            active: true,
        }
    }

    fn deactivate(&mut self) {
        self.active = false;
    }
}

fn main() {
    let mut u = User::new("Ada");
    u.deactivate();
    let u2 = User {
        active: true,
        ..u.clone()
    };
    println!("{u:?} {u2:?}");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct User {
    name: String,
}

fn show(u: &User) {
    println!("{}", u.name);
}

fn main() {
    let u = User {
        name: String::from("Ada"),
    };
    show(&u);
    show(&u);
}

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	u := User{Name: "Ada", Active: true}
	u.Active = false
	fmt.Println(u)
}
  • Go 怎么做:字面量 + 导出字段往往就够用。
  • Rust 为什么不同:所有权与可见性迫使你更早定 API 边界。
  • Go 程序员易踩的坑:用“先全 pub、再到处 clone”硬扛编译器。

Q17. 为什么能同时改两个字段,却常不能「借字段 A 再改字段 B」?

Tags: hot struct borrow split-borrow 适用版本: Rust 1.0+

一句话答案:

对结构体字段的直接访问,编译器可以做拆分借用(各字段互不重叠就能同时借);但方法接收者是整个 self,经 &self/&mut self 返回的字段引用会把整棵结构体借住,于是再改另一个字段就冲突。

解答:

直接改两个字段没问题——两次都是短命的独占访问,不重叠:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Pair {
    a: i32,
    b: i32,
}

fn bump_both(p: &mut Pair) {
    p.a += 1;
    p.b += 1;
}

fn main() {
    let mut p = Pair { a: 1, b: 2 };
    bump_both(&mut p);
    println!("{} {}", p.a, p.b);
}

字段级拆分借用也合法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Pair {
    a: i32,
    b: i32,
}

fn main() {
    let mut p = Pair { a: 1, b: 2 };
    let ra = &mut p.a;
    let rb = &mut p.b; // OK:不同字段
    *ra += 10;
    *rb += 20;
    println!("{} {}", p.a, p.b);
}

一换成「方法借出字段」,借用范围变成整个 self:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct Pair {
    a: i32,
    b: i32,
}

impl Pair {
    fn a_ref(&self) -> &i32 {
        &self.a
    }
}

fn main() {
    let mut p = Pair { a: 1, b: 2 };
    let ra = p.a_ref();
    // p.b += 1;
    // error[E0502]: cannot borrow `p` as mutable because it is also borrowed as immutable
    println!("{ra}");
    p.b += 1; // 等 ra 用完再改就行
}

排错:能写 p.a / p.b 就别绕一层返回 &self 字段的 getter;真要长期同时持有多字段,用局部拆分借用,或把数据拆成更小的类型。

Go 对比:

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

import "fmt"

type Pair struct{ A, B int }

func (p *Pair) ARef() *int { return &p.A }

func main() {
	p := &Pair{A: 1, B: 2}
	a := p.ARef()
	p.B++ // Go 允许;别名与数据竞争要靠人看
	fmt.Println(*a, p.B)
}
  • Go 怎么做:指针别名默认允许,靠纪律与 race detector。
  • Rust 为什么不同:借用检查按「借了谁」算;方法签名常把范围放大到整个结构体。
  • Go 程序员易踩的坑:到处写 fn name(&self) -> &str,再在同作用域改别的字段,撞 E0502。

记忆点:

  • 字段直接访问 → 可拆分借用。
  • &self 方法借出字段 → 锁住整个 self。
  • 冲突时:缩短借用、改直接字段访问,或拆类型。

Q18. #[derive(Default)] 和手写 Default 怎么选?

Tags: common struct default derive 适用版本: Rust 1.0+

一句话答案:

字段全都实现了 Default、且「全零/全默认」语义正确时用 derive;任一字段没有 Default、或默认值不是类型默认(例如 active: true),就手写 impl Default。

解答:

derive 要求每个字段都 Default,生成「字段各自 default 再拼起来」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[derive(Debug, Default)]
struct Config {
    retries: u32,     // 0
    verbose: bool,    // false
}

fn main() {
    let c = Config::default();
    println!("{c:?}");
}

需要自定义默认时手写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
struct User {
    name: String,
    active: bool,
}

impl Default for User {
    fn default() -> Self {
        Self {
            name: String::from("anonymous"),
            active: true, // 不是 bool 的 Default(false)
        }
    }
}

fn main() {
    let u = User::default();
    println!("{u:?}");
}

字段含无 Default 的类型时,derive 直接失败(示意):

1
2
3
error[E0277]: the trait bound `MyId: Default` is not satisfied
// #[derive(Default)]
// struct Wrap { id: MyId }

也可只给部分场景提供 User::new(...),不必强行 Default;Default 适合「空配置 / 占位 / ..Default::default() 更新语法」。

Go 对比:

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

import "fmt"

type User struct {
	Name   string
	Active bool
}

func main() {
	var u User // 零值:"" / false
	fmt.Println(u)
}
  • Go 怎么做:var u T 就是零值默认。
  • Rust 为什么不同:没有隐式零值构造;要默认就实现 / derive Default。
  • Go 程序员易踩的坑:以为 User { .. } 能省略字段;Rust 必须写全或 ..Default::default()。

记忆点:

  • 全字段默认且语义对 → derive(Default)。
  • 自定义默认值 → 手写 impl Default。
  • 没有合理默认 → 只提供 new,别硬 derive。

Q19. 字段简写 User { name } 是什么?

Tags: common struct syntax shorthand 适用版本: Rust 1.0+

一句话答案:

当局部变量名与字段名相同时,User { name } 等价于 User { name: name },是初始化(和解构)时的语法糖。

解答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct User {
    name: String,
    age: u32,
}

fn new_user(name: String, age: u32) -> User {
    User { name, age } // 同 User { name: name, age: age }
}

fn main() {
    let u = new_user(String::from("Ada"), 36);
    println!("{} {}", u.name, u.age);
}

解构同样能简写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct User {
    name: String,
    age: u32,
}

fn main() {
    let u = User {
        name: String::from("Ada"),
        age: 36,
    };
    let User { name, age } = u;
    println!("{name} {age}");
}

名字对不上就必须写完整:User { name: n, age: a }。它只省重复标识符,不改变所有权——String 字段照样 move。

Go 对比:

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

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	name := "Ada"
	age := 36
	u := User{Name: name, Age: age} // 必须 Name: name,没有同名简写
	fmt.Println(u)
}
  • Go 怎么做:复合字面量用 Field: value;键值与变量同名也要写两边。
  • Rust 为什么不同:提供字段初始化简写,少打一遍名字。
  • Go 程序员易踩的坑:写成 User { name: } 或漏字段;简写要求标识符完全同名。

记忆点:

  • 变量名 = 字段名 → Type { field }。
  • 不等价于「可选字段」;没写的字段仍要补齐(或 ..)。
  • 解构侧同样可用简写。

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