Rust 入门:一个写了 6 年 Python 的人,聊聊真实体验和踩坑

25 阅读7分钟

上个月我接了个活,写一个日志分析工具,每天处理大概 2000 万行日志。一开始用 Python 写了个原型,跑起来单核吃满、内存飙到 4G,处理完一天的数据要 40 分钟。这玩意儿上线了不得被运维同事骂死?

正好 2026 年了,身边搞基础设施的朋友基本人手一个 Rust 项目。我就想趁这个机会学一下,于是花了三周,从零开始用 Rust 重写了这个工具。

这篇文章不是 Rust 教程(官方的 The Book 写得比我好一万倍),就是站在一个 Python 老用户的视角,聊聊 Rust 哪里让我觉得爽、哪里让我想砸键盘,以及最终的性能对比数据。

先说结果:值不值得学

直接给结论:如果你是 Python 程序员,Rust 值得学,但不要指望一周入门

重写后的对比数据:

指标Python 版Rust 版
处理 2000 万行耗时38 分钟1 分 42 秒
内存峰值3.8 GB220 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.txtpyproject.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()
CStringC 兼容字符串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 程序员的入门建议

磕了三周,总结几条:

  1. 先看 The Rust Programming Language(The Book),官方文档质量极高,中文翻译也不错
  2. 前三天别追求完美代码,编译器让你 clone()clone(),先跑起来
  3. 用 Cursor 或 TRAE 写 Rust,AI 补全对 Rust 的支持现在很成熟,尤其是生命周期标注,它比我熟练多了
  4. 从 CLI 小工具开始,别上来就搞 Web 框架,用 clap + serde 写个命令行工具是最好的练手项目
  5. 心态要对:编译不过是正常的,编译过了基本就对了。跟 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,就一句话:别怕编译器的红字,那是它在帮你