写了 8 年 Python 转 Rust,我被所有权机制折磨了整整一周

1 阅读1分钟

上个月我接了个私活,写一个高并发日志采集服务。本来用 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.12Rust (release)差距
执行时间4.2s0.18s23x
内存峰值380MB8.5MB45x
二进制大小需要解释器2.1MB 单文件-
CPU 占用单核 100%单核 35%-

测完这组数据我人傻了。release 模式的 Rust 性能碾压 Python 是意料之中,但差距这么大还是超出预期。而且编译出来就是单个可执行文件,不需要装运行时,部署体验比 Python 好太多。

一周下来的真实感受

难受的点:

  • 编译慢。改一行等 5 秒,大项目等 30 秒起步
  • 字符串类型太多:String&str&StringCow<str>,搞不清什么时候用哪个
  • 异步生态没 Python 成熟,tokio 的学习曲线挺陡
  • 错误信息虽然详细但信息量大,新手容易懵

爽到的点:

  • 编译通过基本等于没 bug,这种安全感是 Python 给不了的
  • cargo 工具链体验极好,比 pip 强几个量级
  • 性能不优化就已经很炸裂
  • 模式匹配比 if/else 优雅太多

给想入坑的 Python 程序员

  1. 别从官方 The Book 开始。先看 Rust By Example,边看边敲,比啃书快
  2. 前两周别碰生命周期和 trait 对象。用 String 代替 &str,用 clone() 代替借用,先跑起来
  3. 找一个小工具来练手。CLI 工具、文件处理器这种,别一上来就写 Web 服务
  4. 善用 cargo clippy。Rust 版的 pylint,能教你写出更地道的 Rust 代码
# 安装并运行 clippy
rustup component add clippy
cargo clippy

一周下来我还不敢说自己入门了。但至少能写出编译通过的几百行代码,这在 Rust 社区已经算"活下来了"。那个日志采集服务最终交付了,甲方看到内存占用 12MB 的报告,直接多给了我 20% 的费用。

Rust 不会替代我日常用 Python 写脚本,但在需要性能和可靠性的场景,它已经进我工具箱了。