Rust 的使用:从第一个项目到生产服务

一条面向真实交付的 Rust 学习与使用路径:Cargo、所有权、错误处理、测试、并发、CLI、Web 服务和 Python 扩展。

Rust 的使用:从第一个项目到生产服务

学习 Rust 最容易走向两个极端:只背所有权概念,迟迟不写完整程序;或者直接挑战异步 Web 服务,在生命周期、trait 和错误类型之间失去方向。更有效的方式是沿着真实交付路径学习:项目结构 → 数据建模 → 错误处理 → 测试 → 并发 → 与现有生态集成

本文以稳定版工具链为基线。安装、语法和 Cargo 行为应以 Rust 官方安装页The Rust Programming LanguageCargo Book 为准。

安装与创建项目

官方推荐使用 rustup 管理工具链。安装后先检查版本:

rustc --version
cargo --version
rustup show

创建一个二进制项目:

cargo new log-analyzer
cd log-analyzer
cargo run

Cargo 同时承担依赖管理、编译、测试、文档和发布任务:

Cargo.toml  →  依赖与项目元数据
src/main.rs →  二进制入口
src/lib.rs  →  可复用库入口
tests/      →  集成测试
benches/    →  基准测试(通常配合第三方框架)

常用命令应尽早形成肌肉记忆:

cargo check     # 快速类型检查,不生成最终二进制
cargo test      # 运行测试
cargo fmt       # 格式化
cargo clippy    # 静态检查
cargo build --release
cargo doc --open

用类型表达约束

Rust 项目不应把所有输入都保留为 String。例如日志级别只有有限状态,可以用枚举:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Level {
    Info,
    Warn,
    Error,
}

struct Record {
    level: Level,
    message: String,
}

这样,后续代码不需要反复判断 "error""ERROR" 或拼写错误。Rust 的 enum 可以携带数据,适合表达协议状态、任务结果和领域事件。

当值可能不存在时使用 Option<T>,当操作可能失败时使用 Result<T, E>。这两个类型会迫使调用方显式处理缺失和失败,而不是依赖 null 或异常在远处爆炸。

所有权:先理解移动,再理解借用

Rust 中每个值都有所有者。值被移动后,原变量通常不能再使用:

let name = String::from("rust");
let moved = name;
// println!("{name}"); // 编译错误:所有权已移动
println!("{moved}");

函数只需要读取数据时,传借用而不是转移所有权:

fn count_errors(records: &[Record]) -> usize {
    records.iter().filter(|record| record.level == Level::Error).count()
}

&[Record] 表示只读切片,调用者仍拥有原数据。需要修改时使用 &mut T,但同一时刻对同一数据的可变访问受到严格限制。先掌握“谁拥有数据、函数需要读还是改”,大多数生命周期问题就不会神秘。

错误处理不要只用字符串

命令行程序可以从标准库错误开始:

use std::{fs, io, path::Path};

fn read_input(path: &Path) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

应用层需要组合多种错误时,可以定义领域错误或使用社区库。关键原则是:

  • 库代码返回结构化错误,让调用者决定如何处理;
  • CLI 或服务入口负责添加上下文并转换为用户可理解的信息;
  • 不要在可恢复路径中大量使用 unwrap()
  • 不要吞掉错误后返回空结果。

? 运算符不是忽略错误,而是把错误沿当前函数签名向上传播。

迭代器通常比索引循环更安全

Rust 的迭代器是零成本抽象的重要组成部分。下面的代码读取日志、过滤错误并统计关键词:

fn count_timeout_errors(records: &[Record]) -> usize {
    records
        .iter()
        .filter(|record| record.level == Level::Error)
        .filter(|record| record.message.contains("timeout"))
        .count()
}

迭代器减少手动索引和边界处理,也更容易并行化或替换数据源。性能判断仍应依赖 benchmark 和 profiler,而不是假设链式调用一定更快。

测试与文档是工具链的一部分

单元测试可以和实现放在同一模块:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counts_only_error_records() {
        let records = vec![
            Record { level: Level::Info, message: "ready".into() },
            Record { level: Level::Error, message: "failed".into() },
        ];
        assert_eq!(count_errors(&records), 1);
    }
}

公开 API 的 /// 文档注释可以包含可执行示例,cargo test 会运行文档测试。这让示例与代码演进保持同步,是 Rust 工具链非常实用的特性。Rustdoc Book

并发:先用线程和消息,再引入 async

CPU 密集任务可从标准线程开始:

use std::thread;

let worker = thread::spawn(|| expensive_parse());
let result = worker.join().expect("worker panicked");

如果任务主要等待网络、磁盘或大量连接,async runtime 更合适。但 async 不是“自动更快”:

场景 优先选择
少量 CPU 密集任务 线程池 / 数据并行
大量网络连接 async runtime
简单 CLI 同步代码
GPU 推理 设备运行时 + 明确的请求调度

不要在第一次学习所有权时同时引入 async、宏框架和复杂 trait。先写同步版本并建立测试,再根据性能数据演进。

写一个可交付的 CLI

一个生产级 CLI 至少应包含:

  1. 明确的参数和 --help
  2. 非零退出码表示失败;
  3. 错误写入 stderr,结果写入 stdout;
  4. 可测试的库逻辑与很薄的 main
  5. cargo build --release 的发布构建。

推荐结构:

src/main.rs  → 参数解析、输出、退出码
src/lib.rs   → 解析、转换、领域逻辑
tests/       → 从用户视角调用二进制或库

这种边界比“把所有代码写进 main”更容易复用到 Web 服务或 Python 扩展中。

在 Python 项目中使用 Rust

AI 和数据项目通常不需要迁移整个 Python 代码库。可以只把性能热点下沉:

use pyo3::prelude::*;

#[pyfunction]
fn count_bytes(input: &[u8]) -> usize {
    input.len()
}

#[pymodule]
fn fastops(module: &Bound<'_, PyModule>) -> PyResult<()> {
    module.add_function(wrap_pyfunction!(count_bytes, module)?)?;
    Ok(())
}

PyO3 提供 Rust/Python 绑定,maturin 可以构建 wheel。正确流程是先 profiling,再迁移热点,并用相同输入验证结果一致性。

Web 服务与可观测性

Rust Web 服务的框架选择会变化,工程原则相对稳定:

  • 配置从环境变量或配置文件读取,不把密钥编译进二进制;
  • 为请求设置超时、大小上限和取消机制;
  • 使用结构化日志与 request ID;
  • 对数据库连接池和下游调用设置容量边界;
  • 健康检查区分“进程存活”和“依赖可用”;
  • 优雅停机时停止接收新请求并等待在途任务。

Rust 的类型系统无法替代这些生产设计,但可以让状态与错误边界更明确。

依赖与供应链

crates.io 是 Rust 的公共包注册表,Cargo.lock 用于记录精确依赖版本。应用项目通常应提交 Cargo.lock;库项目是否提交需结合官方 Cargo FAQ 与项目策略。Cargo FAQ

引入依赖前至少检查:

  • 最近发布时间和维护状态;
  • 文档、测试和最低支持 Rust 版本;
  • feature 是否默认引入不需要的能力;
  • 是否包含 unsafe,其边界是否清晰;
  • 许可证是否符合项目要求。

截至 2026 年 7 月 13 日,crates.io Summary API 返回约 29.9 万个 crate。生态丰富意味着选择多,也意味着需要主动管理依赖,而不是按下载量盲选。crates.io API

推荐学习顺序

Cargo 与基础语法
      ↓
所有权、借用、集合
      ↓
Option / Result / 错误设计
      ↓
trait、泛型、模块与测试
      ↓
CLI 或文件处理项目
      ↓
线程 / async / Web / FFI(按需求选择)

最重要的不是一次学完语言,而是完成一个边界清楚的小工具:读取真实输入、返回真实错误、有测试、能构建 release。完成这个闭环后,再进入 async、宏和跨语言集成,Rust 的规则会从“编译器阻碍”变成设计反馈。

参考资料