Rust 经常被拿来谈内存安全,但 Andy Brinkmeyer 这场分享的重点更进一步:真正可靠的系统不只是“不段错误”,还要把资源生命周期、状态转换、调用顺序这些容易在运行时翻车的协议,尽量变成编译器能检查的事实。对工程负责人和架构师来说,这意味着 Rust 不只是替换 C/C++ 的安全语言,也可以成为设计失败更少的系统边界的工具。
内存安全只是起点
内存安全解决的是一大类底层问题:悬垂指针、越界访问、数据竞争等。Rust 的所有权和借用检查器让这些问题很难进入生产环境。
但真实系统的失败常常不是因为某个指针坏了,而是因为协议被错误使用:
- 文件已经关闭后还继续写入。
- 网络连接未握手就发送业务消息。
- 事务没有 commit 或 rollback 就离开作用域。
- 某个配置对象尚未校验就被交给运行时。
这些问题在很多语言里通常靠文档、注释、运行时异常或测试兜底。Rust 的价值在于,它允许我们把这些“必须按顺序做”的规则编码到类型系统里,让错误调用在编译阶段被拒绝。
用 enum 明确失败,而不是把失败藏起来
健壮系统需要正视失败。Rust 的 Result<T, E> 和 enum 鼓励开发者把错误路径写成显式类型,而不是依赖异常从任意位置冒出来。
可以这样实践:为业务错误建模,而不是到处返回字符串。
#[derive(Debug)]
enum PaymentError {
InvalidAmount,
NetworkUnavailable,
RejectedByProvider { code: String },
}
fn charge(amount_cents: u64) -> Result<String, PaymentError> {
if amount_cents == 0 {
return Err(PaymentError::InvalidAmount);
}
// 示例:真实系统这里会调用支付服务
if amount_cents > 100_000 {
return Err(PaymentError::RejectedByProvider {
code: "LIMIT_EXCEEDED".to_string(),
});
}
Ok("payment_123".to_string())
}
fn main() {
match charge(150_000) {
Ok(id) => println!("charged: {id}"),
Err(PaymentError::InvalidAmount) => println!("amount must be positive"),
Err(PaymentError::NetworkUnavailable) => println!("try again later"),
Err(PaymentError::RejectedByProvider { code }) => {
println!("provider rejected payment: {code}")
}
}
}
运行方式:
cargo new robust-errors
cd robust-errors
# 将上面的 Rust 代码替换 src/main.rs
cargo run
这里的关键不是语法,而是工程约束:当你新增一个错误变体时,match 可以迫使调用方重新审视处理逻辑。这比“日志里多了一个新字符串”可靠得多。
Typestate:让非法状态无法表达
分享中特别值得关注的是 typestate pattern。它的思想很直接:对象处在不同状态时,使用不同类型表示;只有合法状态才暴露对应方法。
假设一个连接必须先 connect,再 authenticate,之后才能 send。在普通实现里,你可能会在运行时检查状态:
if state != Authenticated { return error; }
Typestate 的做法是让未认证连接根本没有 send 方法。
use std::marker::PhantomData;
struct Disconnected;
struct Connected;
struct Authenticated;
struct Client<State> {
endpoint: String,
_state: PhantomData<State>,
}
impl Client<Disconnected> {
fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
_state: PhantomData,
}
}
fn connect(self) -> Client<Connected> {
println!("connected to {}", self.endpoint);
Client {
endpoint: self.endpoint,
_state: PhantomData,
}
}
}
impl Client<Connected> {
fn authenticate(self, token: &str) -> Result<Client<Authenticated>, &'static str> {
if token.is_empty() {
return Err("empty token");
}
println!("authenticated");
Ok(Client {
endpoint: self.endpoint,
_state: PhantomData,
})
}
}
impl Client<Authenticated> {
fn send(&self, message: &str) {
println!("sent to {}: {}", self.endpoint, message);
}
}
fn main() -> Result<(), &'static str> {
let client = Client::new("https://api.example.test")
.connect()
.authenticate("secret-token")?;
client.send("deploy");
Ok(())
}
如果你尝试这样写:
fn main() {
let client = Client::new("https://api.example.test");
client.send("deploy");
}
编译器会直接报错,因为 Client<Disconnected> 没有 send 方法。这个错误不需要压测、不需要集成环境、不需要线上告警;它在代码进入主干前就暴露了。
所有权也能管理真实资源
Rust 的所有权不只管理内存,也适合管理文件句柄、锁、事务、socket、临时目录等真实资源。核心手段是 RAII:资源跟随对象生命周期,离开作用域时自动释放。
可以这样设计一个事务保护器:
struct Transaction {
committed: bool,
}
impl Transaction {
fn begin() -> Self {
println!("BEGIN");
Self { committed: false }
}
fn execute(&self, sql: &str) {
println!("EXECUTE: {sql}");
}
fn commit(mut self) {
println!("COMMIT");
self.committed = true;
}
}
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed {
println!("ROLLBACK");
}
}
}
fn main() {
let tx = Transaction::begin();
tx.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
// 注释掉下一行可以看到自动 ROLLBACK
tx.commit();
}
这个例子为了说明模式而简化了数据库细节。落到生产代码里,你可以把真实连接、事务对象或文件句柄包进结构体,并用 Drop 兜住清理路径。不过要注意:Drop 里不能优雅处理异步失败,也不适合隐藏复杂的业务补偿逻辑。它适合释放资源,不适合替代明确的提交、确认或审计流程。
采用建议:把类型系统用在最容易出错的边界
不是每段 Rust 代码都需要 typestate。过度类型化会让 API 变重,泛型和生命周期也可能提高学习成本。更现实的策略是先挑高风险边界:
- 有严格调用顺序的协议,例如连接、握手、认证、关闭。
- 有外部资源生命周期的模块,例如锁、事务、文件、临时凭证。
- 错误处理复杂且经常遗漏分支的业务流程。
- 安全、计费、部署、数据迁移等失败代价高的路径。
架构层面可以用一个简单检查表:这个状态能不能用类型表达?这个错误能不能用 enum 穷举?这个资源能不能绑定到所有权生命周期?这个非法调用能不能让编译器拒绝?
Rust 的健壮性并不来自某个神奇关键字,而来自一种设计取向:把系统协议写进代码结构,让编译器承担更多机械检查,把人的注意力留给真正困难的业务判断。