Rust 程序员的前半年:从 hello world 到系统工具发布的历程
一、2026 年 1 月 3 日,我的第一行 Rust 代码
那天我把 Rust 装好,打开 VS Code,照着《The Rust Programming Language》第一章写了这段代码:
fn main() {
println!("Hello, world!");
}
编译花了 45 秒。我盯着终端的 cargo run 输出,看到那个熟悉的 Hello, world!,心里想的是:"这就完了?和 Python 也没区别啊。"
三天后我就知道错了。错的离谱。
我在写一个文件读写的程序,Python 10 行搞定的事,Rust 编译器给了我 47 行错误。关键词包括 borrow、lifetime、ownership、move——我一个都不懂。那天晚上我失眠了,躺在床上想的是:"我一个写 PHP 的,为什么要来碰 Rust?"
半年后,2026 年 7 月 31 日,我的 dayuan 项目在 GitHub 上有了 270 个 star,我在 CSDN 上写了累计 40 万字的 Rust 技术文章。回头看那个失眠的夜晚,我想对当时的自己说:"这 47 行错误不是终点,是你理解计算机底层的第一课。"
这篇文章是我半年的转码历程复盘。我不打算写成"从零到精通"的成功学,而是想诚实地记录:哪些阶段最痛苦、哪些方法真的有效、哪些"常识"对来说根本不存在。
二、半年路线图
三、四个最痛苦的阶段和我是怎么熬过来的
阶段一:语法关 —— "PHP 10 行,Rust 100 行"
第一个月的最大冲击不是所有权,是类型系统。PHP 里我写 $data = json_decode($response) 从来不用管类型。Rust 里我必须明确 HashMap<String, serde_json::Value>,而且 JSON 解析失败不是抛异常而是返回 Result。
// 这是我 1 月份写的第一段"完整"代码,花了两天才调通
use serde::Deserialize;
/// 定义 API 返回的数据结构
#[derive(Deserialize, Debug)]
struct ApiResponse {
code: i32,
message: String,
data: Option<Vec<User>>, // data 可能是空的!
}
/// 定义用户结构
#[derive(Deserialize, Debug)]
struct User {
id: u64,
name: String,
email: String,
}
/// 调用 API 并解析返回数据
fn fetch_users() -> Result<Vec<User>, Box<dyn std::error::Error>> {
// ① 发起 HTTP 请求,可能失败
let response = reqwest::blocking::get("https://api.example.com/users")?;
// ?: 如果请求失败,直接返回错误
// ② 解析 JSON,可能失败
let api_response: ApiResponse = response.json()?;
// ?: 如果 JSON 格式不对,返回错误
// ③ 检查业务状态码
if api_response.code != 200 {
return Err(format!("API 返回错误: {}", api_response.message).into());
// ^^^ return Err(...): 显式返回错误,不是抛异常
}
// ④ 提取数据,data 可能为空
Ok(api_response.data.unwrap_or_default())
// ^^ Ok(...): 显式返回成功结果
// unwrap_or_default(): 如果 data 是 None,返回空 Vec
}
这段代码现在看起来很简单,但 1 月份我花了整整两天。每个 ?、每个 Result、每个 Option 对我都是全新的概念。
熬过这关的方法:每弄懂一个概念就写一个最小化代码示例,存到 ~/rust-notes/ 目录。到月底这个目录有 47 个文件——每一个都是一个独立的知识点卡片。
阶段二:所有权崩溃 —— "编译器比产品经理还难伺候"
2 月份我开始写一个简单的 CLI 工具,遇到了所有权问题:
fn process_files(paths: Vec<String>) {
let mut config = Config::default(); // config 在这里
for path in &paths {
let content = std::fs::read_to_string(path).unwrap();
// 想把这个文件处理结果存到 config 里
config.add_result(path, content); // ❌ config 被 move 了?
// 下一轮循环 config 就不能用了
}
}
// 当时我唯一的解决方案就是:
// let config = config.clone(); // 到处 clone,性能差,代码丑
我不懂为什么 PHP 里 $config 随便传都不会出问题,而 Rust 里传一次就"死"了。这个问题折磨了我整整两周。
真正理解的转折点:有天我在 B 站看到一个 UP 主画了一张图解释栈和堆的区别。他说"Rust 的所有权不是设计规则,是物理规律——栈上的数据复制快,堆上的数据复制慢,所以 Rust 默认不帮你复制堆数据"。我反复听了三遍,突然明白了。
阶段三:异步编程 —— "tokio::spawn 为什么一直报错?"
3 月份我想给 dayuan 加异步功能,结果被 async/.await 和 Tokio 的组合拳打得怀疑人生:
// 当时报错的代码(精简版)
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
loop {
let (socket, _) = listener.accept().await.unwrap();
// ❌ 编译报错:future cannot be sent between threads safely
tokio::spawn(async {
handle_connection(socket).await;
});
}
}
我花了三天才明白:tokio::spawn 要求闭包是 Send + 'static,而我的 handle_connection 持有了非 Send 的类型。解决方案是确保所有跨线程的类型都实现了 Send trait。
/// ✅ 正确做法:确保所有捕获的类型都是 Send
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// 把全局配置用 Arc 包装,线程安全共享
let config = std::sync::Arc::new(load_config());
loop {
let (socket, _) = listener.accept().await.unwrap();
let config = config.clone(); // 只 clone Arc,不 clone 内部数据
tokio::spawn(async move { // move 把 config 所有权搬进新 task
handle_connection(socket, config).await;
});
}
}
阶段四:从"写对"到"写优雅" —— trait 和泛型
这个阶段没有前三个阶段那么痛,但更花时间。因为前三个阶段有明确的对错边界(编译过 or 不过),而这个阶段要问的是"这样写好还是那样写好"——没有编译器告诉你答案。
/// 4 月份的代码:到处重复
fn format_user_json(user: &User) -> String { /* ... */ }
fn format_user_toml(user: &User) -> String { /* ... */ }
fn format_user_yaml(user: &User) -> String { /* ... */ }
/// 6 月份的代码:trait 统一抽象
trait Formatter {
fn format(&self, user: &User) -> String;
}
struct JsonFormatter;
impl Formatter for JsonFormatter {
fn format(&self, user: &User) -> String {
serde_json::to_string_pretty(user).unwrap()
}
}
struct TomlFormatter;
impl Formatter for TomlFormatter {
fn format(&self, user: &User) -> String {
toml::to_string(user).unwrap()
}
}
// 使用时:多态,添加新格式只需增加一个实现
fn serialize_users(users: &[User], formatter: &dyn Formatter) -> Vec<String> {
users.iter().map(|u| formatter.format(u)).collect()
}
四、的优势和劣势:一个诚实的复盘
真正的劣势
- 计算机基础薄弱:不懂操作系统、不懂编译原理、不懂网络协议栈。这些问题在遇到"为什么连接超时"、"为什么内存不够"时会被放大。
- 缺少 mentor:没有人告诉你"这个不用学"、"那个先跳过"。我踩了很多坑——比如花了一周学
nom(解析器组合子),最后发现 serde 已经完全满足我的需求。 - 调试能力差:不会用 GDB/LLDB,不理解 core dump,遇到段错误就懵了。
被低估的优势
- 没有"应该怎么做"的预设:我不会觉得"Rust 应该像 C++ 那样"或者"异步应该像 Go 那样"。我从零开始接受 Rust 的哲学,反而少了很多阻抗。
- 知道用户要什么:我以前做 PHP 后端,天天和产品经理、前端打交道。这让我在设计 CLI 工具时天然关注用户体验——错误信息能不能让人看懂?配置文件放哪里最直观?
- 敢于用 AI 辅助:科班同学可能觉得用 AI 是"作弊",但我没有这个包袱。每天和 GPT-4 讨论代码设计已经成了我的工作流。
五、总结
这半年的路线如果用一句话总结,就是:
第 1-3 个月,你在学 Rust 的"规矩"。第 4-6 个月,你在用 Rust 的"规矩"解决真实问题。
前半段痛苦但必须经历,后半段享受但需要前半段的积累。没有那 47 行看懂的编译错误,就没有 dayuan 里流畅的 error propagation。没有对着 Send、Sync、Pin 发呆的三天三夜,就写不出稳定的异步服务。
如果你也在转 Rust,我想给你三个建议:
- 第一周不要看任何高级内容。只写
main.rs,只写 loop/if/match/struct,写到闭着眼睛都能写对为止。 - 第三周开始看所有权,但做好心理准备:你会在接下来的两个月里反复理解同一件事,这是正常的。
- 第二个月开始用 Rust 改写你原来语言的代码。实战驱动理解,这比我纯看书快了至少三倍。
最后说一句私心话:转 Rust 最大的挑战不是技术,是在身边的同行都在劝你"别浪费时间"的时候,坚持继续敲下去。
资料说明
本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论,不应视为行业事实。可参考 0731 资料来源索引,并在发布前将具体来源贴到对应断言之后。

945

被折叠的 条评论
为什么被折叠?



