说在前面:我不是那种"学新语言跟喝水一样"的大佬。我写 Python 写了快 5 年,FastAPI、Django、数据处理、爬虫,基本上 Python 能干的活我都干过。2026 年初,团队有个性能敏感的服务要重写,leader 说"要不试试 Rust",我想着反正有 Claude Code 帮忙写,能有多难?
结果第一周我就被所有权系统教做人了。
这篇文章不是 Rust 教程(官方 The Book 比我写得好一万倍),而是一个 Python 程序员学 Rust 时真实的心路历程和踩坑记录。如果你也是动态语言背景,想试试 Rust,希望能帮你少走点弯路。
为什么要学 Rust
先说动机,不然显得我没事找事。
我们有一个日志解析服务,Python 写的,每天处理大概 2000 万条日志。之前还撑得住,但最近数据量翻了一倍,Python 的 GIL 加上 JSON 解析的开销,CPU 直接打满。用了 multiprocessing 开多进程,内存又爆了。
当时摆在面前的选择:
- Go — 团队有人会,上手快
- Rust — 没人会,但性能天花板高
- C++ — 2026 年了,真没必要给自己找罪受
最后选了 Rust,原因很简单:这个服务一旦写完基本不怎么改,追求的是极致性能和内存安全,Rust 刚好对口。Go 的 GC 停顿在我们这个场景下也会有影响。
Python 思维 vs Rust 思维:根本不是一个物种
这是我学 Rust 最大的感受。不是语法难,是思维方式完全不同。
graph TD
A[Python 思维] --> B[变量随便赋值]
A --> C[垃圾回收帮你管内存]
A --> D[运行时才报错]
A --> E[一切皆引用]
F[Rust 思维] --> G[所有权必须明确]
F --> H[你自己管内存,编译器帮你检查]
F --> I[编译时就把错误拦住]
F --> J[移动语义是默认行为]
第一个坑:变量"用了就没了"
Python 里你不会遇到这种问题:
data = [1, 2, 3]
process(data)
print(data) # 完全没问题,data 还在
Rust 里,同样的逻辑直接报错:
fn main() {
let data = vec![1, 2, 3];
process(data);
println!("{:?}", data); // 编译错误!data 已经被 move 了
}
fn process(v: Vec<i32>) {
println!("processing: {:?}", v);
}
编译器会告诉你:value used here after move。
我第一次看到这个错误的时候,真的懵了。什么叫"move"?我就是传了个参数啊?
Rust 的所有权规则就三条:每个值有且只有一个所有者;值在任一时刻只能有一个可变引用,或多个不可变引用;所有者离开作用域,值被自动释放。
Python 程序员可以这么理解:Rust 里把变量传给函数,就像你把房子钥匙给了别人,自己就没钥匙了。想保留?要么给别人一把备用钥匙(引用 &),要么复制一套房(.clone())。
修复后的代码:
fn main() {
let data = vec![1, 2, 3];
process(&data); // 借用,不转移所有权
println!("{:?}", data); // OK,data 还是你的
}
fn process(v: &Vec<i32>) {
println!("processing: {:?}", v);
}
第二个坑:生命周期标注
这是我差点放弃 Rust 的地方。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
编译器说:missing lifetime specifier。我心想,你自己推导不出来吗?两个参数都是引用,返回其中一个,这有什么难理解的?
但编译器确实推导不出来——它不知道返回值的生命周期跟 x 走还是跟 y 走。得显式告诉它:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
这个 'a 就是生命周期标注,意思是"返回值至少活得跟 x 和 y 中较短的那个一样久"。
说实话,我花了整整两天才真正理解这个东西。后来总结了一个规律:只要函数签名里有引用输入和引用输出,大概率要标生命周期。编译器报错了就加,加到不报错为止——虽然有点暴力,但前期真的管用。
让我"哦!"的那些时刻
学 Rust 不全是痛苦,有些设计确实让我觉得对路。
模式匹配 + 枚举
Python 里处理可能为空的值:
result = get_user(user_id)
if result is not None:
print(result.name)
else:
print("not found")
问题是忘了判空,Python 不会提醒你,直到运行时炸一个 AttributeError。
Rust 的 Option 枚举从根上解决了这个问题:
fn get_user(id: u32) -> Option<User> {
// ...
}
match get_user(42) {
Some(user) => println!("{}", user.name),
None => println!("not found"),
}
漏掉 None 分支,编译器直接报错。空指针异常在 Rust 里基本不存在。写了 5 年 Python 被 NoneType has no attribute xxx 折磨过无数次,看到这个设计的时候真的有被感动到。
错误处理:Result + ? 运算符
Python 的 try-except 有个问题:你不知道一个函数到底会抛什么异常,除非去翻文档(而且文档经常不全)。
use std::fs;
use std::io;
fn read_config() -> Result<String, io::Error> {
let content = fs::read_to_string("config.toml")?;
Ok(content)
}
这个 ? 是语法糖,意思是"如果出错了就提前返回错误,没出错就把值拿出来"。函数签名里的 Result<String, io::Error> 明确告诉你:这个函数可能失败,失败原因是 IO 错误。类型系统强制你处理错误,而不是像 Python 那样先跑起来再说。
一周下来的真实感受
爽的:
- 编译通过基本等于程序能跑,运行时惊喜极少
cargo是我用过最顺手的包管理工具,对比 pip 和 poetry 那个混乱局面,差距不是一点半点- 性能是真的猛,日志解析的核心逻辑用 Rust 重写了一小段,处理速度是 Python 的 40 倍左右,内存占用只有十分之一
抓狂的:
- 编译速度太慢,中等项目
cargo build首次编译要两三分钟,增量也要十几秒。Python 根本没有"编译"这个概念,保存就能跑 - 所有权加生命周期的学习曲线确实陡,前三天基本在跟编译器吵架
- 字符串类型有
String、&str、&String、Cow<str>……到现在有时候还会搞混 - 生态跟 Python 的 PyPI 比还是差很多,数据处理和 ML 领域尤其明显
给 Python 程序员的几条建议
1. 先把 The Book 前 10 章老老实实看完
别上来就抄 AI 生成的代码跑。不理解所有权,AI 给你的代码你改都不会改。我一开始让 Claude Code 帮我写,生成的代码能跑,但完全看不懂为什么要加 &、为什么要 .clone(),出了问题两眼一抹黑。
2. 别急着用高级特性
Trait、泛型、生命周期标注、宏——前一周都不用管。先用最笨的方式写:到处 .clone(),用 String 而不是 &str,能 unwrap() 就先 unwrap()。先跑起来,再优化。
3. 善用编译器的错误提示
Rust 编译器的错误信息是我见过最友好的,不仅告诉你哪错了,还告诉你怎么改:
help: consider borrowing here: `&data`
认真读每一条 error 和 help,比查 Stack Overflow 快多了。
4. 找一个实际项目练手
推荐写个 CLI 工具。用 clap 做参数解析,serde 做 JSON 处理,tokio 做异步 IO,Rust 的核心概念基本都能覆盖到。
最终结果
那个日志解析服务,我花了大概三周写完 Rust 版本(后两周顺畅多了)。上线后的数据:
- CPU 使用率从 85% 降到 12%
- 内存从 4GB 降到 400MB
- 处理延迟从 P99 800ms 降到 P99 20ms
- 两个月了,没崩过一次
这个结果说实话有点震撼。开发速度确实比 Python 慢不少,但对于"写一次跑很久"的基础服务,投入产出比真的高。
我现在的策略是:快速迭代、业务逻辑用 Python;性能敏感、长期运行的底层服务用 Rust。两者不冲突,甚至可以通过 PyO3 互相调用。
Rust 填补了一个 Python 填不了的坑。如果你也被 GIL 和内存问题折磨过,值得试试。前三天会很痛苦,挺过去就好了。