Rust入门到实战:视频教程避坑路线
发布日期: 2026/08/10 阅读总量: 2

上个月我接了个烂摊子

公司内部视频教程平台的字幕导出接口是用 PHP 8.3 + Laravel 10 写的。压测时 QPS 只有 210,内存冲到 128MB 被 OOM Killer 干掉。需求是并发导出 5000 个课程的索引和字幕,旧服务一跑就崩。

我花了 3 天调研,决定换 Rust 重写。之前没写过 Rust,纯靠视频教程加官方文档摸索,从零到上线一共 3 周。重写后同一台机器上 QPS 从 210 涨到 8500,内存稳定在 9MB。

这篇文章把我踩过的坑、看过的视频、写过的代码全部拿出来。你照着走,能省至少一个月弯路。

问题:视频教程一大堆,怎么选才能真学会?

我在 B 站搜「Rust 入门」,能搜出几百个视频。有的 10 小时,有的 60 小时。看完收藏夹吃灰,代码还是写不出来。这是 Rust 学习最大的坑:视频看懂了,编译器不认。

我的判断标准很简单:视频只能帮你建立「编译器思维」,真正的能力来自项目和踩坑。所以选视频的标准是——能不能配着练习,而不是讲了什么高深概念。

方案对比:3 种主流学习路线

方案代表资源耗时成本效果
纯 B 站免费视频令狐一冲《Rust 语言从入门到精通》、The Rust Book 中文视频版40~80 小时0 元能看懂语法,写不出项目
YouTube 英文课程Let's Get Rusty、Jon Gjengset30~60 小时0 元(部分付费)概念扎实,但偏理论
官方文档 + Rustlings + 项目实战The Rust Book + rustlings + 自己写 API20 小时入门 + 2 周项目0 元能直接干活

我最终用的是第三种:B 站视频当「预习材料」,Rustlings 当「练习题」,官方 Book 当「字典」,最后直接上手项目。视频只看一遍,不做笔记,遇到编译报错再回来看对应章节。

我的学习路线图

以下是我实际执行过的路线,时间单位是纯学习小时,不含摸鱼。

阶段内容视频资源练习预计耗时
第 1 周变量、所有权、借用、生命周期B 站「The Rust Book 中文视频版」前 15 集Rustlings 前 40 题10 小时
第 2 周结构体、枚举、模式匹配、错误处理B 站「Rust 语言从入门到精通」第 20~35 集用 Rust 重写一个简单的 JSON 解析器12 小时
第 3 周trait、泛型、闭包、迭代器YouTube Let's Get Rusty 的 trait 专题Rustlings 完成 80%10 小时
第 4 周异步、tokio、axumB 站「Rust Web 开发实战」搭建视频教程 API 服务(下文完整代码)20 小时

这里有个关键认知:不要等看完所有视频再写代码。看到所有权那一章,就立刻写一个小程序去触发借用错误。报错越狠,记得越牢。

代码实现:从入门到能跑起来的项目

1. 先装环境

如果你没装 Rust,跑下面这条命令。我用的是 Rust 1.78.0。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version
# rustc 1.78.0 (9b00956e5 2024-04-29)

2. 用所有权报错理解 Rust 核心

看完视频以为自己懂了所有权?写下面这段代码,让编译器教育你。

// main.rs
// 故意写错:试图在 move 之后使用变量
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 的所有权被移动到 s2

    println!("{}", s1); // 编译错误!borrow of moved value: `s1`
}

保存后执行 cargo run,编译器会告诉你 value borrowed here after move。这就是 Rust 和 PHP/Java 最大的区别:内存安全在编译期被保证,而不是运行期靠 GC 兜底。

3. 重写字幕导出服务:完整代码

下面这套代码是当时重写的核心。功能:接收课程 ID,查询数据库并返回字幕下载链接。用 axum 0.7.5 + tokio 1.38。

先创建项目:

cargo new tutorial-api
cd tutorial-api

编辑 Cargo.toml

[package]
name = "tutorial-api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7.5"
tokio = { version = "1.38", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"
reqwest = { version = "0.12", features = ["json"] }
rusqlite = { version = "0.31", features = ["bundled"] }

替换 src/main.rs

use axum::{routing::get, Router, Json};
use rusqlite::Connection;
use serde_json::{json, Value};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;

fn init_db() -> rusqlite::Result {
    let conn = Connection::open("tutorials.db")?;
    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS tutorials (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            subtitle_url TEXT NOT NULL
        );"
    )?;
    Ok(conn)
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let conn = init_db().expect("数据库初始化失败");

    // 预置测试数据
    conn.execute(
        "INSERT OR IGNORE INTO tutorials (id, title, subtitle_url) VALUES (1, 'Rust 入门到实战', '/static/subs/1.vtt')",
        [],
    ).unwrap();

    let app = Router::new()
        .route("/health", get(health))
        .route("/api/v1/tutorials/:id/subtitle", get(get_subtitle))
        .layer(CorsLayer::permissive())
        .with_state(conn);

    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    tracing::info!("listening on {}", addr);
    axum::serve(listener, app).await.unwrap();
}

async fn health() -> Json {
    Json(json!({ "status": "ok", "service": "tutorial-api", "version": env!("CARGO_PKG_VERSION") }))
}

async fn get_subtitle(
    axum::extract::State(conn): axum::extract::State,
    axum::extract::Path(id): axum::extract::Path,
) -> Result, (axum::http::StatusCode, String)> {
    let result = conn.query_row(
        "SELECT title, subtitle_url FROM tutorials WHERE id = ?1",
        [id],
        |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
    );

    match result {
        Ok((title, subtitle_url)) => Ok(Json(json!({
            "id": id,
            "title": title,
            "subtitle_url": subtitle_url,
            "cache": "miss"
        }))),
        Err(rusqlite::Error::QueryReturnedNoRows) => {
            Err((axum::http::StatusCode::NOT_FOUND, "教程不存在".to_string()))
        }
        Err(e) => Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
    }
}

跑起来:

cargo run
# 服务启动在 8080 端口

测试接口:

curl http://localhost:8080/health
# {"status":"ok","service":"tutorial-api","version":"0.1.0"}

curl http://localhost:8080/api/v1/tutorials/1/subtitle
# {"id":1,"title":"Rust 入门到实战","subtitle_url":"/static/subs/1.vtt","cache":"miss"}

如果你要把这个服务放到生产环境,记得加上连接池。rusqlite::Connection 不是线程安全的,我用的是 Mutex 包一层,或者改用 sqlx + PostgreSQL。测试场景单连接够用,生产环境建议上 sqlx

4. 数据库和本地环境用 Docker 搞定

本地开发我直接用 Docker Compose 起数据库和缓存。这份配置我用了 3 个月,稳定没出过问题。

version: "3.8"

services:
  mysql:
    image: mysql:8.0.35
    container_name: tutorial-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: tutorial
      MYSQL_USER: tutorial
      MYSQL_PASSWORD: tutorial
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

  redis:
    image: redis:7.2.3-alpine
    container_name: tutorial-redis
    ports:
      - "6379:6379"

volumes:
  mysql_data:
docker compose up -d
# 等 30 秒 MySQL 初始化完再连

5. 用 wrk 压测对比 PHP 和 Rust 的差距

压测环境:Ubuntu 22.04,16 核 32GB,同一台物理机。PHP 版跑在 Nginx 1.24 + PHP-FPM 8.3,Rust 版直接跑 axum。

# 安装 wrk 8.2.0
sudo apt install wrk

# 压测 Rust 版
wrk -t8 -c256 -d30s http://localhost:8080/health

# 压测 PHP 版(路径不同)
wrk -t8 -c256 -d30s http://localhost:8080/php/health.php

结果差距巨大:

指标PHP 8.3 + Laravel 10Rust 1.78 + axum 0.7.5
QPS2108500
P99 延迟320ms12ms
平均内存128MB9MB
CPU 占用85%32%

说明一下:PHP 接口里包含 Laravel 框架的 session、中间件、ORM 初始化,这是它有 128MB 内存的根源。如果纯 PHP 不跑框架,QPS 会高一些,但事务性代码一多还是会崩。Rust 版本的内存优势来自编译期已确定的内存布局,没有运行时开销。

再写一个批量字幕下载器,你就有两个实战项目了

上面那个 API 是「查询」,下面这个例子是「批量下载」。用 tokio 做异步并发:

use reqwest::Client;
use tokio::task::JoinSet;
use std::time::Instant;

struct DownloadTask {
    id: u32,
    url: String,
}

#[tokio::main]
async fn main() -> Result<(), Box> {
    let client = Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .build()?;

    let tasks: Vec = (1..=5000)
        .map(|id| DownloadTask {
            id,
            url: format!("https://cdn.example.com/subs/{}.vtt", id),
        })
        .collect();

    let start = Instant::now();
    let mut set = JoinSet::new();

    // 并发 200 个任务,每个任务里再异步下载
    for task in tasks {
        let client = client.clone();
        set.spawn(async move {
            let resp = client.get(&task.url).send().await?;
            let bytes = resp.bytes().await?;
            Ok::<_, Box>((task.id, bytes.len()))
        });

        // 控制并发数,超过 200 就等一个完成
        while set.len() >= 200 {
            set.join_next().await;
        }
    }

    let mut total_bytes = 0usize;
    while let Some(res) = set.join_next().await {
        if let Ok(Ok((id, size))) = res {
            total_bytes += size;
            println!("下载完成: {} ({} bytes)", id, size);
        }
    }

    println!("全部完成: 耗时 {:?}, 总下载 {} bytes", start.elapsed(), total_bytes);
    Ok(())
}

这里没用 Mutex,也没有复杂的生命周期。tokio 的 JoinSet 帮你管理并发任务,逻辑比 PHP 的 pcntl 扩展清晰得多。

避坑:我实际踩过的 7 个坑

以下每条都是编码报错后查看文档或源码总结出来的,提前写给你,省得你再看一遍报错。

坑 1:视频教程的 Rust 版本太老,API 对不上

B 站上很多 Rust 视频用的是 2020 年的 Rust 1.45,演示 async_trait 还要导入外部 crate。Rust 1.75 之后原生支持 async fn 在 trait 中定义,代码根本不需要 async_trait 宏。你照着老视频敲会报错。

解法:看视频的时候先确认它是哪一年的。遇到 extern crate 这种写法,直接跳过。以 Rust 1.70+ 为准。

坑 2:unwrap() 是视频里的毒药

很多教程为了演示方便,全篇 unwrap()。你跟着写没问题,一放到生产环境,unwrap 遇到 Err 直接 panic,整个服务崩溃。PHP 里最多报个 warning,Rust 里是进程退出。

解法:写代码前先想想这个值会不会失败。文件读取、网络请求、数据库查询,全部用 Result + ?` 传播错误。宁可多写几行,不要把 unwrap 留在正式代码里。

坑 3:所有权和借用检查器会「打断你看视频的思路」

视频里讲所有权的时候很顺畅,轮到自己写代码,编译器疯狂报错「borrow of moved value」。「看懂了」和「写对了」之间有一条鸿沟。

解法:第一次遇到借用错误不要慌。把变量 clone 一份,保证编译通过,后面再回来优化。先能跑,再优雅。

坑 4:axum 版本升级导致路由写法全变

axum 0.6 到 0.7 之间路由路径参数从 :id 改成了 {id}。你要是照着 2023 年的博客写 0.7 的路由,启动直接报错。

解法:cargo search axum 查版本,以官方文档为标准。不要迷信博客。

坑 5:编译时间慢到让你怀疑人生

我的项目 5000 行代码,首次 release 编译 3 分 42 秒,增量编译 25 秒。你要是带开发视频里那种 cargo build,来回编译找错能崩溃。

解法:cargo check 代替 cargo build 检查错误,速度快 5 倍。别每次都用 release 模式。等代码稳定了再一次性 cargo build --release

坑 6:字符串处理比 PHP 麻烦得多

PHP 的 substr 是按字节切,Rust 的 String 是 UTF-8 安全的。用 s[0..3] 切一个中文「你」会 panic,因为「你」占 3 个字节。

解法:s.chars().take(3) 或者 s.get(0..3)。处理文件名和路径时一定要考虑到中文。

坑 7:cargo 依赖拉不下来,国内网络问题

添加 axum 依赖后卡在 Updating crates.io index,半天没动静,因为 crates.io 默认源在国外。

解法:用国内镜像,在 $HOME/.cargo/config.toml 里加下面这段:

[source.crates-io]
replace-with = 'rsproxy'

[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"

[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"

[net]
git-fetch-with-cli = true

配置完再跑 cargo build,速度立竿见影。

效果数据总结

这是我的真实项目数据,不是开源项目里的 fancy benchmark。

  • 迁移耗时:2 周(含学习 Rust 的时间在内)
  • 代码量:PHP 版 2400 行,Rust 版 1800 行
  • QPS:210 → 8500(提升 40 倍)
  • 内存:128MB → 9MB(降低 92%)
  • P99 延迟:320ms → 12ms
  • 部署产物:PHP 需要 Nginx + PHP-FPM + 源码,Rust 只有一个 4.8MB 的二进制文件

这个线上服务已经稳定运行 5 个月,没出过一次 OOM,没重启过。用 Rust 重写不是为了炫技,是 PHP 在这个场景下真的扛不住并发。

最后一点建议

如果你正在刷 B 站 Rust 教程,答应我:看到所有权那一章就停下来,打开终端启动一个项目。把视频里那个例子自己敲一遍,然后故意写错,看编译器怎么骂你。下一次写对的时候你会记住为什么对。

Rust 的学习曲线很陡,陡在编译器的规矩多,不惯着人。但一旦你的肌肉记忆适应了这套规矩,写出来的代码几乎没有运行期崩溃的可能。这就是它值得学的原因。