上个月我接了个私活,写一个高并发日志采集服务。本来用 Python + asyncio 三天搞定,但甲方要求"内存占用必须控制在 50MB 以内,单机吞吐 10 万条/秒"。Python 再怎么优化也够呛,于是硬着头皮开了个 Rust 项目。
说实话,之前看 Rust 的所有权机制觉得"这有啥难的",真正写起来才知道——编译器是真的会骂人。这篇文章记录一下我这个 Python 老兵转 Rust 第一周的真实体验,不吹不黑。
先说结论:值不值得学?
值得,但别指望一周入门。我写了 8 年 Python,第一周有效代码产出大概是 Python 的 1/5,剩下时间都在跟编译器吵架。但一旦编译通过,程序的稳定性和性能确实让我震到了——同样的日志采集逻辑,Rust 版内存占用 12MB,Python 版 180MB。
环境搭建:比 Python 简单
这倒是出乎我意料。Rust 的工具链比 Python 生态干净太多了。
# 安装 Rust 工具链
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 验证安装
rustc --version
cargo --version
# 创建项目
cargo new log_collector
cd log_collector
不用管 virtualenv、conda、pyenv 这些东西,rustup 一把梭。cargo 同时是包管理器、构建工具、测试运行器,相当于 pip + setuptools + pytest 合体。
Python 思维 vs Rust 思维:第一个坑
我写的第一段 Rust 代码,完全是 Python 思维:
fn main() {
let msg = String::from("hello log");
let processed = process(msg);
println!("原始消息: {}", msg); // 编译报错!
println!("处理结果: {}", processed);
}
fn process(s: String) -> String {
format!("[LOG] {}", s)
}
编译器直接给我甩了一脸红字:
error[E0382]: borrow of moved value: `msg`
--> src/main.rs:4:33
|
2 | let msg = String::from("hello log");
| --- move occurs because `msg` has type `String`
3 | let processed = process(msg);
| --- value moved here
4 | println!("原始消息: {}", msg);
| ^^^ value borrowed here after move
Python 里把变量传给函数后还能继续用,因为 Python 用的是引用计数 + GC。Rust 的所有权机制规定:一个值在同一时刻只能有一个"主人",msg 传给 process 后所有权就转移了,原来的 msg 直接失效。
修复方式有三种:
// 方案 1:传引用(最常用)
fn process(s: &String) -> String {
format!("[LOG] {}", s)
}
// 调用:let processed = process(&msg);
// 方案 2:克隆(简单粗暴,有性能开销)
let processed = process(msg.clone());
// 方案 3:用完再还回来(不推荐,丑)
fn process(s: String) -> (String, String) {
let result = format!("[LOG] {}", s);
(s, result)
}
所有权心智模型
搞明白这张图之后,后面踩的坑少了很多:
graph TD
A[创建值 let s = String::from] --> B{怎么传递?}
B -->|移动 move| C[所有权转移<br/>原变量失效]
B -->|不可变借用 &s| D[只读访问<br/>可以有多个]
B -->|可变借用 &mut s| E[可读写<br/>同时只能有一个]
C --> F[新所有者负责释放]
D --> G[借用结束后<br/>原所有者继续有效]
E --> G
用 Python 的话翻译一下:
- 移动(Move):类似 Python 里
del原变量后把值给新变量 - 不可变借用(&T):类似 Python 里传参但约定函数内不能改
- 可变借用(&mut T):类似 Python 里传参允许修改,但 Rust 保证同一时刻只有一个人能改
真实踩坑:生命周期标注
所有权搞明白后,我以为自己毕业了。结果写到结构体持有引用的时候,又被生命周期(Lifetime)按在地上摩擦。
// 这段代码编译不过
struct LogEntry {
level: &str,
message: &str,
}
编译器要求你标注生命周期:
struct LogEntry<'a> {
level: &'a str,
message: &'a str,
}
fn create_entry<'a>(level: &'a str, msg: &'a str) -> LogEntry<'a> {
LogEntry {
level,
message: msg,
}
}
fn main() {
let entry = create_entry("INFO", "server started");
println!("[{}] {}", entry.level, entry.message);
}
那个 'a 看着吓人,其实就是告诉编译器:"这个结构体里的引用,活得不会比外面的数据更久。"
我的建议:刚入门别硬刚生命周期。 结构体里尽量用 String 而不是 &str,等写了几千行代码有感觉了再优化:
// 新手友好版,先跑起来再说
struct LogEntry {
level: String,
message: String,
}
真正爽到的地方:模式匹配和错误处理
Python 里处理错误基本靠 try/except,很容易忘记处理某些异常。Rust 的 Result + 模式匹配直接在编译期逼你把所有情况都考虑到:
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
fn main() {
match read_config("config.toml") {
Ok(content) => {
println!("配置内容: {}", content);
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
println!("配置文件不存在,使用默认配置");
}
Err(e) => {
eprintln!("读取配置失败: {}", e);
std::process::exit(1);
}
}
}
还有个 ? 操作符,相当于自动 unwrap + 向上抛错,写起来特别爽:
fn load_and_parse(path: &str) -> Result<Vec<String>, io::Error> {
let content = fs::read_to_string(path)?; // 出错自动返回 Err
let lines: Vec<String> = content
.lines()
.map(|l| l.to_string())
.collect();
Ok(lines)
}
Python 里要实现类似效果,得自己写一堆 try/except 嵌套,或者上 returns 这种第三方库。Rust 原生支持,而且零运行时开销。
性能对比:让我决定继续学下去的数据
同样的逻辑——读取 100 万行日志文件,按关键词过滤,写入新文件:
| 指标 | Python 3.12 | Rust (release) | 差距 |
|---|---|---|---|
| 执行时间 | 4.2s | 0.18s | 23x |
| 内存峰值 | 380MB | 8.5MB | 45x |
| 二进制大小 | 需要解释器 | 2.1MB 单文件 | - |
| CPU 占用 | 单核 100% | 单核 35% | - |
测完这组数据我人傻了。release 模式的 Rust 性能碾压 Python 是意料之中,但差距这么大还是超出预期。而且编译出来就是单个可执行文件,不需要装运行时,部署体验比 Python 好太多。
一周下来的真实感受
难受的点:
- 编译慢。改一行等 5 秒,大项目等 30 秒起步
- 字符串类型太多:
String、&str、&String、Cow<str>,搞不清什么时候用哪个 - 异步生态没 Python 成熟,
tokio的学习曲线挺陡 - 错误信息虽然详细但信息量大,新手容易懵
爽到的点:
- 编译通过基本等于没 bug,这种安全感是 Python 给不了的
cargo工具链体验极好,比 pip 强几个量级- 性能不优化就已经很炸裂
- 模式匹配比 if/else 优雅太多
给想入坑的 Python 程序员
- 别从官方 The Book 开始。先看 Rust By Example,边看边敲,比啃书快
- 前两周别碰生命周期和 trait 对象。用
String代替&str,用clone()代替借用,先跑起来 - 找一个小工具来练手。CLI 工具、文件处理器这种,别一上来就写 Web 服务
- 善用
cargo clippy。Rust 版的 pylint,能教你写出更地道的 Rust 代码
# 安装并运行 clippy
rustup component add clippy
cargo clippy
一周下来我还不敢说自己入门了。但至少能写出编译通过的几百行代码,这在 Rust 社区已经算"活下来了"。那个日志采集服务最终交付了,甲方看到内存占用 12MB 的报告,直接多给了我 20% 的费用。
Rust 不会替代我日常用 Python 写脚本,但在需要性能和可靠性的场景,它已经进我工具箱了。