Rust 面试指南

针对有 Go 背景的开发者, 覆盖 Rust 求职面试高频考点

说明: 答案提供核心要点, 面试时需要展开说明


一、基础概念 (30题)

1. Rust 的所有权系统是什么? 解决了什么问题?

核心要点:

  • 每个值有且只有一个所有者
  • 所有者离开作用域, 值被自动释放
  • 解决内存安全问题 (悬空指针、二次释放、内存泄漏) 且无 GC 开销
  • 与 Go GC 对比: Rust 编译时确定, 无运行时暂停, 可预测延迟

2. 什么是借用? 借用规则是什么?

核心要点:

  • 借用 = 不移动所有权的引用 (&T 或 &mut T)
  • 规则: 同一时刻 多个不可变借用 或 一个可变借用 (二选一)
  • 所有借用的生命周期必须在被借用值的生命周期内
  • NLL (Non-Lexical Lifetimes): 实际有效范围不超过最后使用点

3. String 和 &str 有什么区别?

核心要点:

  • String: 堆分配, 可增长, 拥有所有权
  • &str: 切片引用, 可指向 String/字面量/任意 UTF-8 内存, 不拥有数据
  • 函数参数优先用 &str (更灵活), 需要拥有/修改用 String
  • 类比 Go: string ≈ &str, []byte (可修改) ≈ String

4. Clone 和 Copy 的区别?

核心要点:

  • Copy: 标记 trait, 按位复制, 赋值不移动所有权, 隐式发生
  • Clone: 显式调用 .clone(), 可有自定义逻辑, 通常更昂贵
  • Copy 必须 Clone, 反之不然
  • 包含堆数据的类型 (String, Vec) 不能是 Copy

5. 解释 Rust 中的 lifetime (生命周期)?

核心要点:

  • 编译器跟踪引用有效期的机制
  • 大多数情况下自动推断 (elision rules)
  • 需要手动标注: 函数有多个引用参数且返回引用时
  • 生命周期不改变实际存活时间, 只是帮助编译器验证
  • 'static 生命周期: 整个程序存活期 (字符串字面量, static 变量)

6. Option<T> 和 Result<T, E> 分别在什么场景使用?

核心要点:

  • Option<T>: 值可能不存在, 无错误信息 (类比 Go 的 nil, 但强制处理)
  • Result<T, E>: 操作可能失败, 带错误信息 (类比 Go 的 value, err)
  • ? 运算符: 错误提前返回的语法糖
  • Go 中忽略 err 的习惯在 Rust 中会有警告/错误

7. 什么是 trait? 和 Go 的 interface 有什么不同?

核心要点:

  • Trait: 行为规范, 类型实现 trait 即拥有该行为
  • 相同点: 都支持接口多态
  • 不同点:
    • Rust: 可以有默认方法实现; 孤儿规则 (orphan rule) 限制第三方实现
    • Go interface: 隐式实现; Rust 显式 impl Trait for Type
    • Rust 支持关联类型、关联常量
    • Rust 有 object safety 限制

8. 静态分发 vs 动态分发?

核心要点:

  • 静态分发 impl Trait / 泛型: 编译时单态化, 零运行时开销, 代码膨胀
  • 动态分发 dyn Trait: 运行时 vtable 查找, 有间接调用开销, 代码紧凑
  • Box<dyn Trait> 是胖指针: (数据指针, vtable 指针)
  • 性能敏感且类型已知 → 静态; 需要异构集合 → 动态

9. Send 和 Sync trait 是什么?

核心要点:

  • Send: 类型可以安全地转移到另一个线程 (move || {} 需要)
  • Sync: 类型可以通过引用安全地在多线程间共享 (Arc<T> 需要 T: Sync)
  • 大多数类型自动实现, 除了: Rc<T>, Cell<T>, 裸指针
  • unsafe impl Send for MyType {}: 手动声明线程安全性

10. Box<T>, Rc<T>, Arc<T> 的区别?

核心要点:

  • Box<T>: 单一所有者, 堆分配, 零运行时开销
  • Rc<T>: 多所有者, 引用计数, 单线程
  • Arc<T>: 多所有者, 原子引用计数, 多线程安全
  • 选择: 单线程共享用 Rc, 多线程用 Arc, 简单堆分配用 Box

11. 什么是 Rust 的内部可变性?

核心要点:

  • 通过共享引用 (&T) 进行修改的能力
  • Cell<T>: Copy 类型, 用 get/set, 无借用检查
  • RefCell<T>: 任意类型, 运行时借用检查, 违规 panic
  • Mutex<T>/RwLock<T>: 多线程版本的内部可变性
  • 使用场景: struct 有”逻辑不可变” (如缓存/计数器) 但方法签名是 &self

12. Rust 中的闭包 (closure) 如何捕获变量?

核心要点:

  • 三种 trait:
    • Fn: 不可变借用捕获 (可多次调用)
    • FnMut: 可变借用捕获 (可多次调用但修改状态)
    • FnOnce: 移动所有权捕获 (只能调用一次)
  • move ||: 强制移动捕获 (线程/spawn 时常用)
  • 编译器自动推断, 尽可能用最宽泛的 (Fn > FnMut > FnOnce)

13. 什么是迭代器惰性求值?

核心要点:

  • Rust 迭代器是惰性的, 不调用 collect/sum/for_each 等消费适配器则不执行
  • map, filter, take 等是 适配器 (惰性), 返回新迭代器
  • sum, collect, count 等是 消费者 (触发执行)
  • 零开销抽象: 编译器将迭代器链优化为等效的 loop

14. panic! 和 Result 有什么区别? 何时用哪个?

核心要点:

  • panic!: 不可恢复错误, 打印堆栈并退出 (或 unwind)
  • Result: 可恢复错误, 调用方决定如何处理
  • panic 适用: bug (不应该发生), 测试断言, 参数验证的 “impossible case”
  • Result 适用: IO 错误, 解析失败, 任何调用方可合理处理的错误
  • 库代码优先用 Result, 应用程序可以 unwrap (并记录位置)

15. 解释 Rust 中的模式匹配?

// 核心: match 必须穷举所有可能
 
match value {
    Some(x) if x > 0 => println!("positive: {}", x),  // 守卫
    Some(0) => println!("zero"),
    Some(x) => println!("negative: {}", x),
    None => println!("nothing"),
}
 
// 解构
let (x, y, z) = (1, "hello", 3.14);
let Point { x, y } = point;

16. 什么是零成本抽象 (zero-cost abstraction)?

核心要点:

  • 高级语言特性在运行时没有额外开销 (与手写底层代码相当)
  • 例子: 迭代器 ≈ 手写循环, Trait ≈ 手写 vtable
  • 泛型单态化: 每个具体类型生成专用代码, 无虚拟调用
  • 与 Go 对比: Go 有 interface 的开销; Rust 静态分发无开销

17. 什么是 Newtype 模式?

struct Meters(f64);
struct Centimeters(f64);
 
// 防止混用, 编译时类型安全
fn add_distance(a: Meters, b: Meters) -> Meters {
    Meters(a.0 + b.0)
}
// add_distance(Meters(1.0), Centimeters(100.0));  // 编译错误!

18. 什么是 Deref 强制转换 (Deref Coercion)?

核心要点:

  • 当 T: Deref<Target = U>, &T 可自动转换为 &U
  • 链式转换: Box<String> → String → str
  • 允许: Box<String> 传给 &str 参数的函数
  • 这是为什么 String 方法可以在 &String 上调用

19. Rust 中如何实现多态?

核心要点:

  1. Trait + 泛型约束 (编译时多态, 推荐)
  2. dyn Trait (运行时多态)
  3. 枚举 (封闭集合多态)
  4. 宏 (元编程)

20-30. 更多基础题

题目核心答案
20. 什么是 unsafe?5种超能力; 不是关闭检查而是程序员承担责任
21. 什么是 const fn?编译时可调用的函数; CTFE
22. where 子句的作用?复杂泛型约束的可读性写法
23. 什么是关联类型?trait 内定义的类型占位符; vs 泛型参数
24. 什么是 turbofish ::<>?显式指定泛型参数的语法
25. 如何实现运算符重载?实现 std::ops::* traits
26. 什么是 PhantomData?无运行时开销的类型标记, 影响协变/逆变/生命周期
27. 如何实现链式方法调用?方法返回 Self 或 &mut Self
28. cargo feature flags 是什么?条件编译配置, 管理可选依赖
29. 什么是 non-lexical lifetimes?借用的实际范围 = 最后使用点, 而非作用域结束
30. std::mem::replace vs takereplace 设置新值, take 替换为 Default

二、并发与异步 (10题)

31. Rust 为什么”无畏并发”?

核心要点:

  • 类型系统保证线程安全: Send + Sync + 借用检查器
  • 数据竞争在编译时被检测
  • 所有权规则: 同一数据不能同时被多个线程可变访问
  • 对比 Go: Go 运行时检测数据竞争 (-race), Rust 编译时

32. async/await 和线程的区别?

维度async线程
调度协作式 (yield点)抢占式
内存KB级(栈)MB级
数量百万+ task千级线程
阻塞不阻塞线程 IO阻塞
CPU密集不适合适合
适合场景高并发 IOCPU密集/并行

33. Future 是什么? poll 方法如何工作?

核心要点:

  • Future: 可异步完成的计算, lazy (不主动轮询不执行)
  • poll(): 检查是否完成, 返回 Ready(val) 或 Pending
  • Context/Waker: 注册唤醒器, IO 就绪时通知 runtime 重新 poll
  • async fn 编译为状态机, 每个 .await 是状态转换点

34. Tokio 的工作线程模型?

核心要点:

  • 多线程 runtime: 每个 CPU 核一个 worker 线程
  • Work-stealing: 忙碌的线程从空闲线程的队列偷任务
  • spawn_blocking: 阻塞操作放入专用线程池, 不阻塞工作线程
  • 当前线程 runtime: 单线程, 用于测试或嵌入式

35-40. 更多并发题

题目核心答案
35. mpsc vs broadcast?mpsc: 单消费者; broadcast: 多消费者, 落后者会丢消息
36. 如何避免死锁?固定锁顺序; 使用 try_lock; Mutex 尽量短持有
37. RwLock vs Mutex?读多写少用 RwLock; 写频繁用 Mutex
38. 原子操作 memory ordering?Relaxed/Acquire/Release/AcqRel/SeqCst 由宽松到严格
39. 什么是 backpressure?生产快于消费时的限速机制; 有界 channel
40. tokio::select! 的作用?等待多个 Future, 返回最先完成的那个

三、系统设计题 (5题)

41. 设计一个线程安全的连接池

// 关键点:
// - Arc<Mutex<VecDeque<Connection>>> 存储池
// - 获取连接时 pop, 归还时 push
// - 使用 RAII guard 确保连接归还
// - Semaphore 限制最大连接数
// - 超时等待逻辑
 
struct ConnectionPool {
    connections: Arc<Mutex<Vec<Connection>>>,
    semaphore: Arc<Semaphore>,
    max_size: usize,
}

42. 设计一个高性能日志系统

// 关键点:
// - 异步写入 (主线程非阻塞)
// - 使用 mpsc channel, log调用 send, 后台线程写文件
// - 批量写入 (减少 syscall)
// - 日志滚动 (按大小/时间)
// - 结构化日志 (JSON)
 
// 参考: tracing crate 的设计

43. 如何在 Rust 中实现类型安全的状态机?

// 方法1: 枚举 + match
// 方法2: Typestate 模式 (泛型参数表示状态)
struct Order<S> {
    id: OrderId,
    state: PhantomData<S>,
}
 
struct Pending;
struct Paid;
struct Shipped;
 
impl Order<Pending> {
    fn pay(self) -> Order<Paid> { /* ... */ }
}
 
impl Order<Paid> {
    fn ship(self) -> Order<Shipped> { /* ... */ }
}
// Order<Pending>.ship() 编译错误! 类型安全!

44. 如何设计 Rust 的错误处理?

// 推荐方案:
// 1. 定义域错误类型 (thiserror)
// 2. 应用错误类型 (anyhow)
// 3. 边界用 From 转换
 
// 库:
use thiserror::Error;  // 派生宏定义错误
use anyhow::Result;    // 应用层 Box<dyn Error>
 
#[derive(Error, Debug)]
pub enum ServiceError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("not found: {id}")]
    NotFound { id: u64 },
}

45. 如何在 Rust 中进行性能优化?

核心步骤:

  1. 测量: criterion 基准测试, perf/flamegraph 分析
  2. 算法: 先优化算法复杂度
  3. 数据结构: 减少分配, 提高缓存命中 (连续内存 > 指针追逐)
  4. 并行: rayon 数据并行, tokio IO 并行
  5. SIMD: std::simd 或 packed_simd (特定场景)
  6. 编译策略: lto = "thin", codegen-units = 1

四、代码实现题 (5题)

46. 实现线程安全的单例

use std::sync::OnceLock;
 
struct Config {
    value: String,
}
 
static INSTANCE: OnceLock<Config> = OnceLock::new();
 
fn get_config() -> &'static Config {
    INSTANCE.get_or_init(|| Config {
        value: "default".to_string(),
    })
}

47. 实现带泛型的排序

fn sort_by_key<T, K: Ord>(items: &mut [T], key_fn: impl Fn(&T) -> K) {
    items.sort_by(|a, b| key_fn(a).cmp(&key_fn(b)));
}
 
fn main() {
    let mut words = vec!["banana", "apple", "cherry"];
    sort_by_key(&mut words, |s| s.len());
    println!("{:?}", words);  // ["apple", "banana", "cherry"]
}

48. 实现 Builder 模式

#[derive(Debug)]
struct Request {
    url: String,
    method: String,
    timeout: u64,
    headers: Vec<(String, String)>,
}
 
struct RequestBuilder {
    url: String,
    method: String,
    timeout: u64,
    headers: Vec<(String, String)>,
}
 
impl RequestBuilder {
    fn new(url: impl Into<String>) -> Self {
        RequestBuilder {
            url: url.into(),
            method: "GET".to_string(),
            timeout: 30,
            headers: vec![],
        }
    }
    
    fn method(mut self, m: impl Into<String>) -> Self {
        self.method = m.into(); self
    }
    
    fn timeout(mut self, t: u64) -> Self {
        self.timeout = t; self
    }
    
    fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.headers.push((k.into(), v.into())); self
    }
    
    fn build(self) -> Request {
        Request {
            url: self.url,
            method: self.method,
            timeout: self.timeout,
            headers: self.headers,
        }
    }
}

49. 实现简单的 Future

// 见 09-异步编程 练习5: 手动实现 ManualFuture

50. 反转链表

struct Node {
    val: i32,
    next: Option<Box<Node>>,
}
 
fn reverse(head: Option<Box<Node>>) -> Option<Box<Node>> {
    let mut prev = None;
    let mut current = head;
    
    while let Some(mut node) = current {
        current = node.next.take();
        node.next = prev;
        prev = Some(node);
    }
    
    prev
}

面试评分标准

等级描述能回答的题目
L1 初级理解基础语法和所有权1-15
L2 中级熟练使用 Trait 和并发1-35
L3 高级掌握异步和系统设计1-45
L4 专家能优化性能, 贡献开源1-50 全部

充分准备面试, Rust 职位等你来! 🦀