上个月我接了个活,写一个日志分析工具,每天处理大概 2000 万行日志。一开始用 Python 写了个原型,跑起来单核吃满、内存飙到 4G,处理完一天的数据要 40 分钟。这玩意儿上线了不得被运维同事骂死?
正好 2026 年了,身边搞基础设施的朋友基本人手一个 Rust 项目。我就想趁这个机会学一下,于是花了三周,从零开始用 Rust 重写了这个工具。
这篇文章不是 Rust 教程(官方的 The Book 写得比我好一万倍),就是站在一个 Python 老用户的视角,聊聊 Rust 哪里让我觉得爽、哪里让我想砸键盘,以及最终的性能对比数据。
先说结果:值不值得学
直接给结论:如果你是 Python 程序员,Rust 值得学,但不要指望一周入门。
重写后的对比数据:
| 指标 | Python 版 | Rust 版 |
|---|---|---|
| 处理 2000 万行耗时 | 38 分钟 | 1 分 42 秒 |
| 内存峰值 | 3.8 GB | 220 MB |
| 二进制大小 | N/A(需要解释器) | 4.2 MB |
| CPU 占用 | 单核 100% | 4 核均匀分配 |
| 开发耗时 | 2 天 | 12 天(含学习时间) |
22 倍的速度差距,内存只有 Python 版的 1/17。开发时间多了 5 倍——但这包括了从零学 Rust 的时间,熟练之后估计 3-4 天能写完。
让 Python 程序员觉得爽的部分
Cargo 是我用过最好的包管理器
说真的,Cargo 让我第一次觉得包管理可以这么顺。想想 Python 那边:pip、virtualenv、pyenv、poetry、pdm、uv……光选哪个工具就能吵三天。
Rust 只有一个 Cargo,创建项目、管理依赖、编译、测试、发布全包了:
# 创建项目
cargo new log-analyzer
cd log-analyzer
# 加依赖,直接编辑 Cargo.toml 或者用命令
cargo add serde --features derive
cargo add rayon
cargo add clap --features derive
# 编译运行
cargo run --release
# 跑测试
cargo test
Cargo.toml 长这样,清清爽爽:
[package]
name = "log-analyzer"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
rayon = "1.10"
clap = { version = "4.5", features = ["derive"] }
没有 requirements.txt 和 pyproject.toml 到底该用哪个的灵魂拷问,没有虚拟环境忘了激活导致装错位置的抓狂。这一点 Rust 完胜。
编译器就是最好的 Code Reviewer
rustc 严格到变态,但说实话——它救了我无数次。
Python 里写出一个 None 引用的 bug,可能到线上跑了三天才炸。Rust 在编译阶段就不让你过:
// Python 里你可能这么写
// data = get_data() # 可能返回 None
// print(data.strip()) # 运行时才炸
// Rust 里你必须处理 None 的情况
fn process_line(line: Option<&str>) -> String {
match line {
Some(content) => content.trim().to_string(),
None => String::from("(empty)"),
}
}
一开始我觉得这很烦,每个可能为空的值都要 match 或者 unwrap。但写了一周之后发现:我的 Rust 代码几乎没有运行时错误。编译通过基本就能跑,这种安全感是 Python 给不了的。
模式匹配太香了
Python 3.10 加了 match 语法,但说实话用得很少。Rust 的模式匹配是深入骨髓的,到处都在用:
use std::collections::HashMap;
#[derive(Debug)]
enum LogLevel {
Info,
Warn,
Error(String), // Error 可以携带额外信息
}
fn parse_log_line(line: &str) -> Option<(LogLevel, &str)> {
let parts: Vec<&str> = line.splitn(3, ' ').collect();
match parts.as_slice() {
[_, "INFO", msg] => Some((LogLevel::Info, msg)),
[_, "WARN", msg] => Some((LogLevel::Warn, msg)),
[_, "ERROR", msg] => Some((LogLevel::Error(msg.to_string()), msg)),
_ => None, // 解析失败就返回 None,不会 panic
}
}
fn main() {
let lines = vec![
"2026-04-15 INFO user logged in",
"2026-04-15 ERROR database connection timeout",
"malformed line",
];
let mut stats: HashMap<&str, usize> = HashMap::new();
for line in &lines {
match parse_log_line(line) {
Some((LogLevel::Error(ref detail), _)) => {
println!("发现错误: {}", detail);
*stats.entry("error").or_insert(0) += 1;
}
Some((LogLevel::Warn, _)) => {
*stats.entry("warn").or_insert(0) += 1;
}
Some((LogLevel::Info, _)) => {
*stats.entry("info").or_insert(0) += 1;
}
None => {
*stats.entry("unknown").or_insert(0) += 1;
}
}
}
println!("统计结果: {:?}", stats);
}
每个分支都处理得明明白白,漏掉任何一个分支编译器直接报错。写 Python 的时候我经常忘记处理边界情况,Rust 不给你这个机会。
让我想砸键盘的部分
所有权和借用:从入门到想放弃
这是每个 Rust 新手必经的痛苦。Python 里你随便把变量传来传去,谁也不管谁。Rust 里每个值都有一个"所有者",传出去了就没了:
fn main() {
let name = String::from("hello");
let greeting = make_greeting(name);
// println!("{}", name); // ❌ 编译错误!name 已经被 move 了
println!("{}", greeting); // ✅ 这个没问题
}
fn make_greeting(n: String) -> String {
format!("Hi, {}!", n)
}
我第一天写 Rust 的时候,编译器报了 47 个所有权相关的错误,不夸张。
后来总结了一个心智模型,分享给同样从 Python 过来的朋友:
graph TD
A[创建一个值] --> B{要传给别的函数?}
B -->|只读一下| C["传引用 &value"]
B -->|要修改它| D["传可变引用 &mut value"]
B -->|不再需要原值| E["直接 move"]
B -->|需要各自一份| F["clone 一份"]
C --> G[原值还能继续用]
D --> H[同一时间只能有一个 &mut]
E --> I[原值不能再用了]
F --> J[两份独立的值]
建议:前三天别跟编译器较劲,它让你 clone 你就 clone,先把东西跑起来,再优化。
字符串类型多到怀疑人生
Python 里字符串就是 str,顶多分个 bytes。Rust 里光常见的就有:
| 类型 | 说明 | 类比 Python |
|---|---|---|
String | 堆上分配,可变,拥有所有权 | str(最接近) |
&str | 字符串切片,不可变引用 | 类似字符串视图 |
&[u8] | 字节切片 | bytes |
OsString | 操作系统字符串 | os.fsencode() |
CString | C 兼容字符串 | ctypes 里用的 |
Cow<str> | 写时克隆,可以是借用也可以是拥有 | 没有对应的 |
我前两周写的代码到处是 .to_string()、.as_str()、&*s 这种转换,丑得一批。后来慢慢理解了:函数参数尽量用 &str,返回值和结构体字段用 String,基本能覆盖 80% 的场景。
生命周期标注
劝退率最高的知识点。写一个返回引用的函数时,Rust 要你标注这个引用"活多久":
// 编译器不知道返回的引用跟哪个参数的生命周期绑定
// 所以你得告诉它
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}
fn main() {
let result;
let s1 = String::from("long string");
{
let s2 = String::from("hi");
result = longer(&s1, &s2);
println!("{}", result); // ✅ s2 还活着
}
// println!("{}", result); // ❌ s2 已经没了,result 可能悬空
}
到现在我也没完全吃透复杂的生命周期场景。我的策略是:能用 String 返回就不返回引用,除非你很确定自己在做什么。
真正让我留下来的:Rayon 并行处理
这是让我决定在合适场景用 Rust 的关键原因。
Python 因为 GIL,多线程做 CPU 密集任务基本是摆设。Rust 的 Rayon 库让并行化简单到离谱:
use rayon::prelude::*;
use std::fs;
fn count_errors_in_file(path: &str) -> usize {
let content = fs::read_to_string(path).unwrap_or_default();
content
.par_lines() // 就这一行!把 lines() 换成 par_lines()
.filter(|line| line.contains("ERROR"))
.count()
}
fn main() {
let files: Vec<String> = (1..=100)
.map(|i| format!("logs/app_{}.log", i))
.collect();
let total_errors: usize = files
.par_iter() // 并行遍历文件
.map(|f| count_errors_in_file(f))
.sum();
println!("总共发现 {} 个错误", total_errors);
}
.lines() 换成 .par_lines(),.iter() 换成 .par_iter(),并行就完成了。没有线程池配置,没有 concurrent.futures,没有 GIL。
而且 Rust 的所有权系统保证了你没法写出数据竞争的代码——编译器直接拦住。Python 多线程共享数据踩的那些坑,在 Rust 里根本不存在。
给 Python 程序员的入门建议
磕了三周,总结几条:
- 先看 The Rust Programming Language(The Book),官方文档质量极高,中文翻译也不错
- 前三天别追求完美代码,编译器让你
clone()就clone(),先跑起来 - 用 Cursor 或 TRAE 写 Rust,AI 补全对 Rust 的支持现在很成熟,尤其是生命周期标注,它比我熟练多了
- 从 CLI 小工具开始,别上来就搞 Web 框架,用
clap+serde写个命令行工具是最好的练手项目 - 心态要对:编译不过是正常的,编译过了基本就对了。跟 Python 正好反过来——Python 是写着爽调试哭,Rust 是写着哭跑着爽
什么时候该用 Rust 而不是 Python
| 场景 | 选择 | 理由 |
|---|---|---|
| 快速原型/脚本/数据分析 | Python | 开发速度快 10 倍 |
| API 服务(中小规模) | Python (FastAPI) | 生态成熟,开发效率高 |
| CPU 密集型数据处理 | Rust | 性能差距太大 |
| CLI 工具 | Rust | 编译成单二进制,分发方便 |
| 需要长期运行的服务 | Rust | 内存安全,没有 GC 停顿 |
| AI/ML 相关 | Python | 生态没得比 |
小结
三周下来,我不会说"Rust 是最好的语言"这种话。但确实体会到了一点:需要性能和安全的时候,"编译通过就能放心跑"这种感觉,真的很难回去。
日志分析工具现在稳定跑了两周,内存稳定在 200MB 左右,处理时间从 38 分钟降到不到 2 分钟。运维同事从想打我变成了问我能不能再写几个类似的工具。
如果你也是 Python 背景想试试 Rust,就一句话:别怕编译器的红字,那是它在帮你。