定位
Axum 是 Tokio 官方维护的 Web 框架,建在 hyper 和 tower 之上。它是当前 Rust 后端的默认答案,核心原因是能直接用整个 tower 生态的中间件。
设计极简:一个 handler 就是普通 async fn,参数通过实现 FromRequest 的类型提取。
安装
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
注意 axum 还在 0.x(0.8.9),跨小版本升级有破坏性变更,建议锁版本。
最小示例
use axum::{
Router,
routing::{get, post},
extract::Path,
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct User { id: u64, name: String }
#[derive(Deserialize)]
struct CreateUser { name: String }
async fn hello() -> &'static str {
"Hello"
}
async fn get_user(Path(id): Path<u64>) -> Json<User> {
Json(User { id, name: "demo".into() })
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User { id: 1, name: payload.name })
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/users/{id}", get(get_user))
.route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
注意路径参数语法:axum 0.8 起用 {id},0.7 及以前是 :id。抄旧教程会失败。
核心概念
1. Extract(提取器)
handler 的参数可以是任何实现了 FromRequestParts 或 FromRequest 的类型:
| 类型 | 提取什么 |
|---|---|
Path<T> | 路径参数 |
Query<T> | 查询字符串 |
Json<T> | JSON body |
State<S> | 应用共享状态 |
HeaderMap | 请求头 |
TypedHeader<T> | 类型化的请求头 |
提取器最多只能有一个消费 body 的(比如 Json<T>),而且必须放在参数列表最后。
2. State:共享状态
use axum::extract::State;
#[derive(Clone)]
struct AppState { db: Pool }
async fn handler(State(state): State<AppState>) -> String {
// 用 state.db
String::new()
}
let app = Router::new()
.route("/", get(handler))
.with_state(AppState { db: pool });
AppState 必须实现 Clone——Axum 每次请求都克隆一份。所以里面放大对象要用 Arc 包。
3. 中间件:直接用 tower
这是 Axum 最大的优势。tower 生态的中间件不需要适配:
use tower_http::{
trace::TraceLayer,
cors::CorsLayer,
compression::CompressionLayer,
timeout::TimeoutLayer,
};
use std::time::Duration;
let app = Router::new()
.route("/", get(hello))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new())
.layer(TimeoutLayer::new(Duration::from_secs(10)));
只要库是为 tower 写的,就能直接用。 这是它比 Actix 更受欢迎的核心原因。
常见坑
① 版本号敏感
0.6 → 0.7 改了提取器 trait 名称,0.7 → 0.8 改了路径参数语法。文档和教程混着看会浪费大量时间。以官方 CHANGELOG 为准。
② 编译慢
第一次全量编译 axum + tokio + serde 要几分钟。日常开发用 cargo check,比 cargo build 快得多。
③ 错误处理要自己设计
Axum 没有内置的错误响应格式。惯例是实现 IntoResponse:
enum AppError { NotFound, Internal(anyhow::Error) }
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, msg) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "not found"),
AppError::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
};
(status, msg).into_response()
}
}
handler 返回 Result<T, AppError> 就能统一处理。
④ State 里的锁用错
Arc<Mutex<T>> 在 await 期间持有锁会导致死锁(因为 MutexGuard 不是 Send)。异步场景要用 tokio::sync::Mutex。
什么时候选它
- 绝大多数 Rust 后端场景(生态最主流)
- 需要 tower 中间件(限流、追踪、压缩、超时)
- 和 sqlx、tonic、tracing 配合使用
不选它的场景:需要极致的吞吐(Actix 略强)、只想快速出原型(Rocket 更友好)、团队 Rust 经验不足。
一个现实提醒
Rust Web 开发的成本主要在编译和类型系统,不在框架。
改一行代码等 30 秒编译,这是日常。如果你要反复调 UI 逻辑或业务规则,Rust 的开发效率会明显低于 Go/Node。
真正值得用 Rust 的场景:CPU 密集(图像处理、加解密、协议解析)、内存受限、需要极致低延迟、已经有 Rust 代码库要集成。
否则,Go 能解决的问题用 Rust 做,通常是「付出三倍开发时间换来 10% 的性能提升」。