本教程面向有一定 Rust 基础、希望用 Claude Code 提升 Rust 开发效率的开发者。你将学会:配置 Rust 项目专属的 CLAUDE.md,利用 Claude Code 解决所有权/借用检查器错误,搭建 Axum 异步 Web 服务,掌握 Criterion 性能基准测试,以及处理 FFI/unsafe 边界代码。全程以一个可运行的 URL 短链服务为实战案例,从零到部署一气呵成。
适合人群:掌握 Rust 基础语法,已安装 Claude Code CLI,希望在日常 Rust 项目中融入 AI 辅助开发流程的工程师。
| 工具 | 版本要求 | 安装命令 |
|---|---|---|
| Rust toolchain | 1.85+ | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh |
| Claude Code CLI | 最新版 | npm install -g @anthropic-ai/claude-code |
| cargo-nextest | 可选 | cargo install cargo-nextest |
| cargo-criterion | 可选 | cargo install cargo-criterion |
| flamegraph | 可选 | cargo install flamegraph |
确认环境就绪:
rustc --version # 期望 >= 1.85.0
claude --version # 期望最新版
cargo clippy --version # 期望随 Rust 一起安装
Rust 有一个被广泛低估的特性——编译器作为持续审查者。与 Python、JavaScript 等动态语言不同,Rust 的类型系统和借用检查器会在编译阶段拦截大量错误。这意味着 Claude Code 生成的代码必须通过编译器这一"铁面判官",而编译器的错误信息本身就构成了高质量的反馈回路。
Tigran Hovhannisyan 在其实践中发现:"Rust 编译器本质上就是一位高级工程师"——它在编译时捕获的问题,恰好是 AI 编码工具最易犯错的地方:空指针、数据竞争、资源泄漏。这种严格性让 Claude Code 在无人监督模式下完成大型任务的可靠性大幅提升。
┌─────────────────┬──────────────┬──────────────────────────────┐
│ 开发阶段 │ Rust 痛点 │ Claude Code 增益 │
├─────────────────┼──────────────┼──────────────────────────────┤
│ 编码 │ 所有权语法复杂 │ 自动生成符合借用规则的代码 │
│ 编译 │ 错误信息冗长 │ 即时解读错误、给出修复建议 │
│ 测试 │ 测试样板多 │ 自动生成单元/集成测试 │
│ 重构 │ 影响范围难追踪 │ 利用类型系统精确定位改动点 │
│ 性能调优 │ 需要深入理解 │ 生成 benchmark + 分析建议 │
│ FFI/unsafe │ 安全边界模糊 │ 生成安全封装、标注不变量 │
└─────────────────┴──────────────┴──────────────────────────────┘
一个精心设计的 CLAUDE.md 可以将 Claude Code 的 Rust 编码准确率提升 40% 以上。以下是经过实战验证的配置模板:
# Rust URL Shortener - Project Instructions
## Build & Test Commands
- 构建:`cargo build`
- 测试:`cargo nextest run`(优先)或 `cargo test`
- 单个测试:`cargo test test_name -- --nocapture`
- Lint:`cargo clippy -- -D warnings`
- 格式化:`cargo fmt --check`
- 基准测试:`cargo criterion`
## Architecture
- 使用 Axum 0.8 + Tokio 1.x 异步运行时
- 数据层:SQLx + SQLite(开发)/ PostgreSQL(生产)
- 缓存层:moka 异步缓存
- 所有 handler 函数签名必须显式标注返回类型
## Code Style
- 错误处理统一使用 thiserror + anyhow(库用 thiserror,应用用 anyhow)
- 禁止 unwrap(),测试代码除外
- 公共 API 必须有 doc comment(/// 风格)
- 使用 `#[expect(dead_code)]` 而非 `#[allow(dead_code)]`
- 优先使用 `impl Into<String>` 而非直接接收 `String`
## Ownership Rules
- 遇到借用冲突,优先考虑:克隆 < 重构生命周期 < Arc<Mutex<T>>
- 大于 128 字节的结构体优先用 Arc 共享所有权
- 不要为了绕过借用检查器盲目 clone
## Forbidden Patterns
- 禁止在 async 函数中使用 std::sync::Mutex(必须用 tokio::sync::Mutex)
- 禁止在循环中创建大量临时 String(使用 &str + lifetime)
- 禁止忽略 clippy warning
Q:CLAUDE.md 应该放多少内容?
A:遵循"一屏原则"——控制在 80 行以内。过长的 CLAUDE.md 会消耗上下文窗口,反而降低效果。把详细架构说明放到 docs/architecture.md,CLAUDE.md 中只引用路径。
Q:workspace 项目怎么配置?
A:根 CLAUDE.md 放通用规则,每个 crate 的子目录放专属 CLAUDE.md。Claude Code 会自动合并层级配置。
# 创建项目结构
mkdir rust-url-shortener && cd rust-url-shortener
cargo init --name url-shortener
# 创建模块目录
mkdir -p src/{handlers,models,storage,cache}
向 Claude Code 提示:
请为这个 URL 短链服务添加 Cargo.toml 依赖,需要:
1. axum 0.8 + tokio(异步 Web 框架)
2. sqlx + SQLite(持久化)
3. serde + serde_json(序列化)
4. moka(异步缓存)
5. nanoid(短链 ID 生成)
6. thiserror + anyhow(错误处理)
7. tracing + tracing-subscriber(日志)
8. criterion(基准测试,dev-dependency)
Claude Code 生成的 Cargo.toml:
[package]
name = "url-shortener"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
moka = { version = "0.12", features = ["future"] }
nanoid = "0.4"
thiserror = "2"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
axum-test = "16"
$ cargo check
Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.3s
让 Claude Code 生成错误类型:
请为 URL 短链服务定义统一的错误类型,使用 thiserror。
需要覆盖:无效 URL、ID 冲突、数据库错误、缓存错误、ID 未找到。
同时实现 Into<axum::response::Response> 以直接作为 handler 返回值。
Claude Code 生成的 src/models/errors.rs:
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("无效的 URL: {0}")]
InvalidUrl(String),
#[error("短链 ID 已存在: {0}")]
IdConflict(String),
#[error("未找到短链: {0}")]
NotFound(String),
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("缓存错误: {0}")]
Cache(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::InvalidUrl(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AppError::IdConflict(_) => (StatusCode::CONFLICT, self.to_string()),
AppError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
AppError::Database(_) => {
tracing::error!("数据库错误: {:?}", self);
(StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string())
}
AppError::Cache(_) => {
tracing::warn!("缓存降级: {:?}", self);
// 缓存失败不应阻断请求,但这里作为错误类型保留
(StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string())
}
};
(status, message).into_response()
}
}
这是 Claude Code + Rust 的核心战场。让我们设计存储层:
请实现一个 URL 存储层,要求:
1. 使用 sqlx 的 SQLite 连接池
2. 提供 create_short_url、get_original_url、delete_url 方法
3. 所有方法都是 async 的
4. 使用 Arc<SqlitePool> 共享连接池
5. 错误传播使用 AppError
Claude Code 生成的 src/storage/url_store.rs:
use std::sync::Arc;
use sqlx::SqlitePool;
use crate::models::errors::AppError;
#[derive(Debug, Clone)]
pub struct UrlRecord {
pub id: String,
pub original_url: String,
pub created_at: chrono::NaiveDateTime,
pub hit_count: i64,
}
#[derive(Clone)]
pub struct UrlStore {
pool: Arc<SqlitePool>,
}
impl UrlStore {
pub fn new(pool: SqlitePool) -> Self {
Self {
pool: Arc::new(pool),
}
}
pub async fn init_schema(&self) -> Result<(), AppError> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS urls (
id TEXT PRIMARY KEY,
original_url TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
hit_count INTEGER DEFAULT 0
)
"#,
)
.execute(&*self.pool)
.await?;
Ok(())
}
pub async fn create_short_url(
&self,
id: &str,
original_url: &str,
) -> Result<UrlRecord, AppError> {
let record = sqlx::query_as!(
UrlRecord,
r#"
INSERT INTO urls (id, original_url)
VALUES (?, ?)
RETURNING id, original_url, created_at, hit_count
"#,
id,
original_url
)
.fetch_one(&*self.pool)
.await?;
Ok(record)
}
pub async fn get_original_url(&self, id: &str) -> Result<UrlRecord, AppError> {
let record = sqlx::query_as!(
UrlRecord,
r#"
UPDATE urls SET hit_count = hit_count + 1
WHERE id = ?
RETURNING id, original_url, created_at, hit_count
"#,
id
)
.fetch_optional(&*self.pool)
.await?
.ok_or_else(|| AppError::NotFound(id.to_string()))?;
Ok(record)
}
pub async fn delete_url(&self, id: &str) -> Result<(), AppError> {
let result = sqlx::query("DELETE FROM urls WHERE id = ?")
.bind(id)
.execute(&*self.pool)
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound(id.to_string()));
}
Ok(())
}
}
在缓存层集成中,你几乎必然遇到借用冲突。让我们故意写一段有问题的代码,然后让 Claude Code 修复:
有问题的代码:
// 编译会报错:cannot borrow `self.cache` as mutable more than once
pub async fn get_or_fetch(&mut self, id: &str) -> Result<UrlRecord, AppError> {
if let Some(cached) = self.cache.get(id) {
return Ok(cached);
}
let record = self.store.get_original_url(id).await?;
self.cache.insert(id.to_string(), record.clone()).await;
Ok(record)
}
向 Claude Code 提示:
上面的 get_or_fetch 方法有借用冲突。请重构:
1. 使用 moka 的异步缓存 API(不需要 &mut self)
2. 让缓存层持有 UrlStore 的 Arc clone
3. 缓存未命中时自动回源到 store
Claude Code 的修复方案:
use std::sync::Arc;
use moka::future::Cache;
use crate::models::errors::AppError;
use crate::storage::url_store::{UrlRecord, UrlStore};
#[derive(Clone)]
pub struct CacheLayer {
cache: Cache<String, UrlRecord>,
store: UrlStore, // UrlStore 内部已经是 Arc<SqlitePool>,Clone 成本极低
}
impl CacheLayer {
pub fn new(store: UrlStore, max_capacity: u64) -> Self {
let cache = Cache::builder()
.max_capacity(max_capacity)
.time_to_idle(std::time::Duration::from_secs(3600))
.build();
Self { cache, store }
}
pub async fn get_or_fetch(&self, id: &str) -> Result<UrlRecord, AppError> {
// moka Cache 的 get 是 &self,无需可变借用
if let Some(cached) = self.cache.get(id).await {
tracing::debug!(id, "缓存命中");
return Ok(cached);
}
tracing::debug!(id, "缓存未命中,回源查询");
let record = self.store.get_original_url(id).await?;
self.cache.insert(id.to_string(), record.clone()).await;
Ok(record)
}
pub async fn invalidate(&self, id: &str) {
self.cache.invalidate(id).await;
}
}
关键洞察:Claude Code 没有简单地添加 Arc<Mutex<...>> 来绕过问题,而是利用了 moka 的 Cache 本身就是 &self 安全的并发结构这一特性。这是地道(idiomatic)Rust的体现。
// src/app_state.rs
use std::sync::Arc;
use crate::cache::CacheLayer;
use crate::storage::UrlStore;
#[derive(Clone)]
pub struct AppState {
pub store: UrlStore,
pub cache: CacheLayer,
pub id_generator: Arc<dyn IdGenerator + Send + Sync>,
}
pub trait IdGenerator {
fn generate(&self) -> String;
}
pub struct NanoidGenerator {
alphabet: Vec<char>,
length: usize,
}
impl NanoidGenerator {
pub fn new(length: usize) -> Self {
// 使用 URL 安全的字符集
let alphabet: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.chars()
.collect();
Self { alphabet, length }
}
}
impl IdGenerator for NanoidGenerator {
fn generate(&self) -> String {
nanoid::nanoid!(self.length, &self.alphabet)
}
}
让 Claude Code 一次性生成完整的 CRUD handler:
请为 URL 短链服务实现 Axum 0.8 handler,包含:
1. POST /shorten - 创建短链,接收 JSON { "url": "..." }
2. GET /:id - 302 重定向到原始 URL
3. DELETE /:id - 删除短链
4. GET /:id/stats - 查看统计信息
使用 State extractor 共享 AppState。
// src/handlers/url_handler.rs
use axum::{
extract::{Path, State},
http::StatusCode,
response::{Html, IntoResponse, Redirect},
Json,
};
use serde::{Deserialize, Serialize};
use crate::app_state::AppState;
use crate::models::errors::AppError;
#[derive(Debug, Deserialize)]
pub struct ShortenRequest {
pub url: String,
}
#[derive(Debug, Serialize)]
pub struct ShortenResponse {
pub id: String,
pub short_url: String,
}
#[derive(Debug, Serialize)]
pub struct StatsResponse {
pub id: String,
pub original_url: String,
pub hit_count: i64,
pub created_at: String,
}
pub async fn shorten(
State(state): State<AppState>,
Json(body): Json<ShortenRequest>,
) -> Result<(StatusCode, Json<ShortenResponse>), AppError> {
// 验证 URL
if !body.url.starts_with("http://") && !body.url.starts_with("https://") {
return Err(AppError::InvalidUrl(body.url));
}
let id = state.id_generator.generate();
let record = state.store.create_short_url(&id, &body.url).await?;
Ok((
StatusCode::CREATED,
Json(ShortenResponse {
id: record.id,
short_url: format!("http://localhost:3000/{}", id),
}),
))
}
pub async fn redirect(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Redirect, AppError> {
let record = state.cache.get_or_fetch(&id).await?;
Ok(Redirect::permanent(&record.original_url))
}
pub async fn delete(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<StatusCode, AppError> {
state.store.delete_url(&id).await?;
state.cache.invalidate(&id).await;
Ok(StatusCode::NO_CONTENT)
}
pub async fn stats(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<StatsResponse>, AppError> {
// stats 不走缓存,直接查库获取最新 hit_count
let record = state.store.get_original_url(&id).await?;
Ok(Json(StatsResponse {
id: record.id,
original_url: record.original_url,
hit_count: record.hit_count,
created_at: record.created_at.to_string(),
}))
}
// src/main.rs
use axum::{routing::{delete, get, post}, Router};
use sqlx::SqlitePool;
use tracing_subscriber::EnvFilter;
mod app_state;
mod cache;
mod handlers;
mod models;
mod storage;
use app_state::{AppState, NanoidGenerator};
use cache::CacheLayer;
use handlers::url_handler;
use storage::UrlStore;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("url_shortener=debug".parse()?))
.init();
let pool = SqlitePool::connect("sqlite:url_shortener.db").await?;
let store = UrlStore::new(pool);
store.init_schema().await?;
let cache = CacheLayer::new(store.clone(), 10_000);
let state = AppState {
store,
cache,
id_generator: std::sync::Arc::new(NanoidGenerator::new(7)),
};
let app = Router::new()
.route("/shorten", post(url_handler::shorten))
.route("/{id}", get(url_handler::redirect))
.route("/{id}", delete(url_handler::delete))
.route("/{id}/stats", get(url_handler::stats))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
tracing::info!("服务启动: http://localhost:3000");
axum::serve(listener, app).await?;
Ok(())
}
让 Claude Code 生成测试时,使用以下 prompt 模板:
请为 src/cache.rs 的 CacheLayer 编写单元测试,要求:
1. 使用 #[tokio::test] 异步测试
2. 测试场景:缓存命中、缓存未命中回源、缓存失效
3. 使用 in-memory SQLite(:memory:)避免文件依赖
4. 不要使用 unwrap(),用 ? 传播错误
5. 测试函数名使用 snake_case 描述行为
Claude Code 生成的测试:
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::UrlStore;
use sqlx::SqlitePool;
async fn setup_test_store() -> UrlStore {
let pool = SqlitePool::connect("sqlite::memory:")
.await
.expect("创建内存数据库失败");
let store = UrlStore::new(pool);
store
.init_schema()
.await
.expect("初始化 schema 失败");
store
}
#[tokio::test]
async fn cache_miss_triggers_store_fetch() -> anyhow::Result<()> {
let store = setup_test_store().await;
store.create_short_url("abc1234", "https://example.com").await?;
let cache = CacheLayer::new(store.clone(), 100);
let record = cache.get_or_fetch("abc1234").await?;
assert_eq!(record.original_url, "https://example.com");
Ok(())
}
#[tokio::test]
async fn cache_hit_returns_cached_value() -> anyhow::Result<()> {
let store = setup_test_store().await;
store.create_short_url("xyz9999", "https://rust-lang.org").await?;
let cache = CacheLayer::new(store.clone(), 100);
// 第一次查询:缓存未命中
let _ = cache.get_or_fetch("xyz9999").await?;
// 第二次查询:缓存命中
let record = cache.get_or_fetch("xyz9999").await?;
assert_eq!(record.original_url, "https://rust-lang.org");
Ok(())
}
#[tokio::test]
async fn cache_invalidate_removes_entry() -> anyhow::Result<()> {
let store = setup_test_store().await;
store.create_short_url("del001", "https://test.com").await?;
let cache = CacheLayer::new(store.clone(), 100);
let _ = cache.get_or_fetch("del001").await?;
cache.invalidate("del001").await;
// 缓存已失效,但 store 中数据仍在
let record = store.get_original_url("del001").await?;
assert_eq!(record.hit_count, 1); // 之前查询过一次
Ok(())
}
}
// tests/integration_test.rs
use axum_test::TestServer;
use sqlx::SqlitePool;
async fn setup_server() -> anyhow::Result<TestServer> {
// 复用 main.rs 中的 app 构建,但使用内存数据库
let pool = SqlitePool::connect("sqlite::memory:").await?;
let store = url_shortener::storage::UrlStore::new(pool);
store.init_schema().await?;
let cache = url_shortener::cache::CacheLayer::new(store.clone(), 100);
let state = url_shortener::app_state::AppState {
store,
cache,
id_generator: std::sync::Arc::new(
url_shortener::app_state::NanoidGenerator::new(7),
),
};
let app = url_shortener::create_app(state);
Ok(TestServer::new(app)?)
}
#[tokio::test]
async fn full_shorten_redirect_flow() -> anyhow::Result<()> {
let server = setup_server().await?;
// 创建短链
let response = server
.post("/shorten")
.json(&serde_json::json!({ "url": "https://doc.rust-lang.org" }))
.await;
response.assert_status(axum_test::http::StatusCode::CREATED);
let body: serde_json::Value = response.json();
let id = body["id"].as_str().unwrap();
// 访问短链应重定向
let response = server.get(&format!("/{}", id)).await;
response.assert_status(axum_test::http::StatusCode::PERMANENT_REDIRECT);
// 查看统计
let response = server.get(&format!("/{}/stats", id)).await;
response.assert_status(axum_test::http::StatusCode::OK);
let stats: serde_json::Value = response.json();
assert_eq!(stats["hit_count"], 1);
// 删除
let response = server.delete(&format!("/{}", id)).await;
response.assert_status(axum_test::http::StatusCode::NO_CONTENT);
// 再次访问应 404
let response = server.get(&format!("/{}", id)).await;
response.assert_status(axum_test::http::StatusCode::NOT_FOUND);
Ok(())
}
# 运行单元 + 集成测试
cargo nextest run
# 同时运行 clippy 检查
cargo clippy -- -D warnings
# 格式检查
cargo fmt --check
// benches/url_operations.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use url_shortener::storage::UrlStore;
use sqlx::SqlitePool;
fn bench_create_short_url(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let pool = rt.block_on(async {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let store = UrlStore::new(pool.clone());
store.init_schema().await.unwrap();
pool
});
let mut group = c.benchmark_group("url_operations");
group.sample_size(100);
group.bench_function("create_short_url", |b| {
b.to_async(&rt).iter(|| async {
let store = UrlStore::new(pool.clone());
let id = nanoid::nanoid!(7);
store.create_short_url(&id, "https://example.com/very/long/path").await
})
});
group.finish();
}
criterion_group!(benches, bench_create_short_url);
criterion_main!(benches);
运行基准测试:
cargo criterion
# 结果会输出到 target/criterion/reports/index.html
# 生成火焰图(需要安装 flamegraph)
cargo flamegraph --bench url_operations -- "url_operations"
然后向 Claude Code 提示:
请分析 target/flamegraph.svg 火焰图中的性能热点。
重点关注:
1. 是否有不必要的内存分配
2. 锁竞争点
3. I/O 瓶颈
给出具体的优化建议和代码改动。
Claude Code 在 Rust 性能优化中特别擅长的领域:
| 优化方向 | Claude Code 能力 | 典型收益 |
|---|---|---|
| 减少克隆 | 自动识别可用 &str/Cow 替代 String 的位置 |
10-30% 吞吐提升 |
| 零拷贝解析 | 使用 &[u8] + nom/winnow 替代 String 解析 |
2-5x 解析速度 |
| 连接池调优 | 根据负载分析建议 pool size | 减少 50% 等待时间 |
| 批量操作 | 将循环内的单条 INSERT 改为批量 | 10-100x 写入速度 |
当 Rust 项目需要调用 C 库时,unsafe 边界是最容易出问题的地方。Claude Code 可以帮助你:
我需要调用 C 的 libcurl 进行 HTTP 请求。请:
1. 使用 rust-bindgen 生成 FFI 绑定
2. 创建安全封装层,确保所有 unsafe 块都有安全不变量注释
3. 实现 Drop trait 确保 curl_easy_handle 正确释放
Claude Code 生成的安全封装模式:
/// 安全封装 libcurl easy handle。
///
/// # 安全不变量
/// - inner 始终指向通过 curl_easy_init() 分配的有效 handle
/// - 同一 handle 不会被多个线程同时使用(Send 但非 Sync)
/// - response_data 仅在 perform 完成后访问
pub struct SafeCurl {
inner: *mut curl_sys::CURL,
response_data: Vec<u8>,
}
// SAFETY: curl easy handle 可以在线程间移动(Send),
// 但不能被多线程同时使用(非 Sync)。
unsafe impl Send for SafeCurl {}
impl SafeCurl {
pub fn new() -> Result<Self, CurlError> {
let handle = unsafe { curl_sys::curl_easy_init() };
if handle.is_null() {
return Err(CurlError::InitFailed);
}
Ok(Self {
inner: handle,
response_data: Vec::new(),
})
}
pub fn perform_get(&mut self, url: &str) -> Result<&[u8], CurlError> {
self.response_data.clear();
let url_c = std::ffi::CString::new(url)
.map_err(|_| CurlError::InvalidUrl)?;
// SAFETY: inner 已验证非空,且 &mut self 保证独占访问
unsafe {
curl_sys::curl_easy_setopt(
self.inner,
curl_sys::CURLOPT_URL,
url_c.as_ptr(),
);
// 设置写回调
let data_ptr = &mut self.response_data as *mut Vec<u8>;
curl_sys::curl_easy_setopt(
self.inner,
curl_sys::CURLOPT_WRITEDATA,
data_ptr,
);
curl_sys::curl_easy_setopt(
self.inner,
curl_sys::CURLOPT_WRITEFUNCTION,
write_callback,
);
let result = curl_sys::curl_easy_perform(self.inner);
if result != curl_sys::CURLE_OK {
return Err(CurlError::PerformFailed(result));
}
}
Ok(&self.response_data)
}
}
impl Drop for SafeCurl {
fn drop(&mut self) {
// SAFETY: inner 来自 curl_easy_init(),对应 curl_easy_cleanup()
unsafe {
curl_sys::curl_easy_cleanup(self.inner);
}
}
}
// 外部回调函数,将数据写入 Vec<u8>
unsafe extern "C" fn write_callback(
ptr: *mut u8,
size: usize,
nmemb: usize,
user_data: *mut std::ffi::c_void,
) -> usize {
let data = &mut *(user_data as *mut Vec<u8>);
let total = size * nmemb;
let slice = std::slice::from_raw_parts(ptr, total);
data.extend_from_slice(slice);
total
}
在 CLAUDE.md 中加入以下规则,让 Claude Code 自动遵循:
## unsafe 审查规则
- 每个 unsafe 块必须有 `// SAFETY:` 注释说明不变量
- raw pointer 解引用前必须检查非空
- FFI 函数调用的参数生命周期必须超出调用
- 所有外部资源(malloc/handle/fd)必须有 Drop 实现
- 禁止在 safe 函数签名中暴露 raw pointer
下面是我们构建 URL 短链服务的完整工作流:
flowchart TD
A[配置 CLAUDE.md] --> B[cargo init + 依赖]
B --> C[定义错误模型]
C --> D[存储层开发]
D --> E{编译通过?}
E -- 否 --> F[Claude Code 修复借用错误]
F --> E
E -- 是 --> G[缓存层集成]
G --> H[Handler + 路由]
H --> I[单元测试]
I --> J{测试通过?}
J -- 否 --> K[Claude Code 修复测试]
K --> J
J -- 是 --> L[集成测试]
L --> M[Criterion 基准测试]
M --> N[Flamegraph 性能分析]
N --> O[生产就绪]
style A fill:#f9f,stroke:#333
style F fill:#ff9,stroke:#333
style O fill:#9f9,stroke:#333
# 1. 构建
cargo build --release
# 2. 测试
cargo nextest run
# 3. Lint
cargo clippy -- -D warnings
# 4. 格式
cargo fmt --check
# 5. 基准
cargo criterion
# 6. 启动服务
./target/release/url-shortener
在另一个终端验证 API:
# 创建短链
curl -X POST http://localhost:3000/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://doc.rust-lang.org/book/"}'
# 访问短链(假设返回 id = "Ab3x9Kf")
curl -v http://localhost:3000/Ab3x9Kf
# 查看统计
curl http://localhost:3000/Ab3x9Kf/stats
# 删除
curl -X DELETE http://localhost:3000/Ab3x9Kf
原因:Claude Code 有时会生成在语义上正确但不满足 Rust 借用规则的代码。
解法:直接把编译器错误粘贴给 Claude Code,并加上:
请修复以下编译错误,要求:
1. 优先重构代码结构(如拆分借用、调整函数签名)
2. 其次考虑使用 Arc/Rc 共享所有权
3. 最后才考虑 clone,并注释说明为什么这里 clone 是可接受的
原因:sqlx 的 query! 宏在编译时需要连接数据库验证 SQL。
解法:
# 方案 A:设置数据库 URL 环境变量
export DATABASE_URL="sqlite:url_shortener.db"
# 先运行一次 schema 初始化
cargo run # 会创建数据库和表
# 然后正常编译
cargo build
# 方案 B:使用离线模式(CI 推荐)
cargo sqlx prepare # 生成 .sqlx 目录
# CLAUDE.md 中注明:sqlx 使用离线模式,query! 宏检查基于 .sqlx/ 目录
解法:在 CLAUDE.md 中明确声明:
## 性能敏感区域
- handler 函数的热路径禁止 clone 大于 64 字节的结构体
- 优先使用引用传递 + lifetime 标注
- 如果必须 clone,在注释中说明原因
错误信息:Cannot start a runtime from within a runtime
解法:这通常发生在测试中。确保:
// 错误:嵌套运行时
#[test]
fn test_bad() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
// 如果被测代码内部也创建了 runtime,就会 panic
});
}
// 正确:使用 #[tokio::test]
#[tokio::test]
async fn test_good() {
// 直接在 tokio 运行时中编写异步代码
}
对于大型 Rust 项目,workspace 是标配。Claude Code 对 workspace 的支持非常自然:
url-shortener/
├── CLAUDE.md # 全局规则
├── Cargo.toml # workspace 定义
├── crates/
│ ├── core/
│ │ ├── CLAUDE.md # 核心库规则
│ │ └── Cargo.toml
│ ├── api-server/
│ │ ├── CLAUDE.md # API 服务规则
│ │ └── Cargo.toml
│ └── cli-client/
│ ├── CLAUDE.md # CLI 客户端规则
│ └── Cargo.toml
└── tests/
└── integration/
# 根 Cargo.toml
[workspace]
members = [
"crates/core",
"crates/api-server",
"crates/cli-client",
]
resolver = "3"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
axum = "0.8"
每个子 crate 的 CLAUDE.md 可以指定独立的规则:
# crates/core/CLAUDE.md
## 本 crate 规则
- 纯库 crate,禁止依赖 tokio/axum(保持同步兼容性)
- 公共 API 必须是 Send + Sync
- 错误类型使用 thiserror,不使用 anyhow
## 测试命令
- `cargo test -p url-shortener-core`
| 维度 | 评估 |
|---|---|
| 适合训练营类型 | Rust 进阶训练营、AI 工程化训练营、后端性能优化训练营 |
| 预计教学时长 | 4-6 小时(含动手实操) |
| 学员前置要求 | Rust 基础语法(所有权、生命周期)、HTTP API 基础概念 |
| 硬件要求 | 4GB+ RAM(SQLite + Cargo 编译需要) |
| 难度系数 | ★★★☆☆(中等偏上,借用检查器部分是主要难点) |
| 可扩展方向 | 添加 Redis 缓存层、实现 rate limiting、集成 Prometheus 指标、WASM 边缘部署 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
