rust-backend
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRust Backend Coding Guidelines
Rust后端编码规范
Apply these patterns when writing or modifying Rust code in the directory.
backend/在目录中编写或修改Rust代码时,请遵循以下模式。
backend/Data Structure Design
数据结构设计
Choose between , , or based on domain needs:
structenumnewtype- Use for state machines instead of boolean flags or loosely related fields
enum - Model invariants explicitly using types (e.g., ,
NonZeroU32, custom enums)Duration - Consider ownership of each field:
- Use vs
&str, slices vs vectorsString - Use when sharing across threads
Arc<T> - Use for flexible ownership
Cow<'a, T>
- Use
rust
// State machine with enum
enum JobState {
Pending { scheduled_for: DateTime<Utc> },
Running { started_at: DateTime<Utc>, worker: String },
Completed { result: JobResult, duration_ms: i64 },
Failed { error: String, retries: u32 },
}
// Avoid multiple booleans
struct Job {
is_pending: bool, // Don't do this
is_running: bool,
is_completed: bool,
}根据业务需求选择、或:
structenumnewtype- 用实现状态机,而非布尔标志或松散关联的字段
enum - 使用类型显式建模不变量(例如、
NonZeroU32、自定义枚举)Duration - 考虑每个字段的所有权:
- 选择还是
&str,切片还是向量String - 跨线程共享时使用
Arc<T> - 灵活所有权场景使用
Cow<'a, T>
- 选择
rust
// State machine with enum
enum JobState {
Pending { scheduled_for: DateTime<Utc> },
Running { started_at: DateTime<Utc>, worker: String },
Completed { result: JobResult, duration_ms: i64 },
Failed { error: String, retries: u32 },
}
// Avoid multiple booleans
struct Job {
is_pending: bool, // Don't do this
is_running: bool,
is_completed: bool,
}Impl Block Organization
Impl块组织
Place blocks immediately below the struct/enum they modify. Group methods logically:
implrust
struct JobQueue {
jobs: Vec<Job>,
capacity: usize,
}
impl JobQueue {
// Constructors first
pub fn new(capacity: usize) -> Self { ... }
pub fn with_jobs(jobs: Vec<Job>) -> Self { ... }
// Getters
pub fn len(&self) -> usize { ... }
pub fn is_empty(&self) -> bool { ... }
// Mutation methods
pub fn push(&mut self, job: Job) -> Result<()> { ... }
pub fn pop(&mut self) -> Option<Job> { ... }
// Domain logic
pub fn next_scheduled(&self) -> Option<&Job> { ... }
}将块紧跟在其对应的struct/enum下方。按逻辑分组方法:
implrust
struct JobQueue {
jobs: Vec<Job>,
capacity: usize,
}
impl JobQueue {
// Constructors first
pub fn new(capacity: usize) -> Self { ... }
pub fn with_jobs(jobs: Vec<Job>) -> Self { ... }
// Getters
pub fn len(&self) -> usize { ... }
pub fn is_empty(&self) -> bool { ... }
// Mutation methods
pub fn push(&mut self, job: Job) -> Result<()> { ... }
pub fn pop(&mut self) -> Option<Job> { ... }
// Domain logic
pub fn next_scheduled(&self) -> Option<&Job> { ... }
}Iterator Chains Over For-Loops
优先使用迭代器链而非For循环
Prefer functional iterator chains () over imperative for-loops:
.filter().map().collect()rust
// Preferred
let results: Vec<_> = items
.iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
// Avoid
let mut results = Vec::new();
for item in items.iter() {
if item.is_valid() {
results.push(item.transform());
}
}优先使用函数式迭代器链()而非命令式for循环:
.filter().map().collect()rust
// Preferred
let results: Vec<_> = items
.iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
// Avoid
let mut results = Vec::new();
for item in items.iter() {
if item.is_valid() {
results.push(item.transform());
}
}Error Handling
错误处理
Use the type from . Return or for fallible functions:
Errorwindmill_common::errorResult<T, Error>JsonResult<T>rust
use windmill_common::error::{Error, Result};
// Use ? operator for propagation
pub async fn get_job(db: &DB, id: Uuid) -> Result<Job> {
let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound("job not found".to_string()))?;
Ok(job)
}Prefer for optional handling. Use when early return makes code clearer:
if letlet...elserust
let Some(config) = get_config() else {
return Err(Error::MissingConfig);
};Never panic in library code. Reserve for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity.
.unwrap()使用中的类型。对于可能失败的函数,返回或:
windmill_common::errorErrorResult<T, Error>JsonResult<T>rust
use windmill_common::error::{Error, Result};
// Use ? operator for propagation
pub async fn get_job(db: &DB, id: Uuid) -> Result<Job> {
let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound("job not found".to_string()))?;
Ok(job)
}优先使用处理可选值。当提前返回能让代码更清晰时,使用:
if letlet...elserust
let Some(config) = get_config() else {
return Err(Error::MissingConfig);
};库代码中绝对不要panic。仅在有编译期保证的场景下使用。保持函数简短,以帮助生命周期推断并提升代码清晰度。
.unwrap()Early Returns
提前返回
Return early to avoid deep nesting. Handle error cases and edge conditions first:
rust
// Preferred - early returns
fn process_job(job: Option<Job>) -> Result<Output> {
let Some(job) = job else {
return Ok(Output::default());
};
if !job.is_valid() {
return Err(Error::InvalidJob);
}
if job.is_cached() {
return Ok(job.cached_result());
}
// Main logic at the end, not nested
execute_job(job)
}
// Avoid - deep nesting
fn process_job(job: Option<Job>) -> Result<Output> {
if let Some(job) = job {
if job.is_valid() {
if !job.is_cached() {
execute_job(job)
} else {
Ok(job.cached_result())
}
} else {
Err(Error::InvalidJob)
}
} else {
Ok(Output::default())
}
}提前返回以避免深层嵌套。先处理错误情况和边缘条件:
rust
// Preferred - early returns
fn process_job(job: Option<Job>) -> Result<Output> {
let Some(job) = job else {
return Ok(Output::default());
};
if !job.is_valid() {
return Err(Error::InvalidJob);
}
if job.is_cached() {
return Ok(job.cached_result());
}
// Main logic at the end, not nested
execute_job(job)
}
// Avoid - deep nesting
fn process_job(job: Option<Job>) -> Result<Output> {
if let Some(job) = job {
if job.is_valid() {
if !job.is_cached() {
execute_job(job)
} else {
Ok(job.cached_result())
}
} else {
Err(Error::InvalidJob)
}
} else {
Ok(Output::default())
}
}Variable Shadowing
变量遮蔽
Shadow variables instead of creating new names with prefixes:
rust
// Preferred
let data = fetch_raw_data();
let data = parse(data);
let data = validate(data)?;
// Avoid
let raw_data = fetch_raw_data();
let parsed_data = parse(raw_data);
let validated_data = validate(parsed_data)?;使用变量遮蔽而非带前缀的新变量名:
rust
// Preferred
let data = fetch_raw_data();
let data = parse(data);
let data = validate(data)?;
// Avoid
let raw_data = fetch_raw_data();
let parsed_data = parse(raw_data);
let validated_data = validate(parsed_data)?;Minimal Comments
最小化注释
- No inline comments explaining obvious code
- No TODO/FIXME comments in committed code
- Doc comments () only on public items
/// - Let code be self-documenting through clear naming
- 不为显而易见的代码添加行内注释
- 提交的代码中不要有TODO/FIXME注释
- 仅对公共项使用文档注释()
/// - 通过清晰的命名让代码自文档化
Type Safety
类型安全
Use enums over boolean flags for clarity:
rust
// Preferred
enum JobStatus {
Pending,
Running,
Completed,
}
// Avoid
struct Job {
is_running: bool,
is_completed: bool,
}为了清晰性,用枚举替代布尔标志:
rust
// Preferred
enum JobStatus {
Pending,
Running,
Completed,
}
// Avoid
struct Job {
is_running: bool,
is_completed: bool,
}Pattern Matching
模式匹配
Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields:
rust
// Explicit matching preferred
match status {
JobStatus::Pending => handle_pending(),
JobStatus::Running => handle_running(),
JobStatus::Completed => handle_completed(),
}
// Wildcards OK for fallback
match result {
Ok(value) => process(value),
Err(_) => return default_value(),
}
// Wildcards OK for ignoring fields in destructuring
let Point { x, y, .. } = point;优先使用显式匹配。在回退场景或忽略字段时策略性地使用通配符:
rust
// Explicit matching preferred
match status {
JobStatus::Pending => handle_pending(),
JobStatus::Running => handle_running(),
JobStatus::Completed => handle_completed(),
}
// Wildcards OK for fallback
match result {
Ok(value) => process(value),
Err(_) => return default_value(),
}
// Wildcards OK for ignoring fields in destructuring
let Point { x, y, .. } = point;Destructuring in Function Signatures
函数签名中的解构
Destructure structs directly in function parameters:
rust
// Preferred
async fn process_job(
Extension(db): Extension<DB>,
Path((workspace, job_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Job>> {
// ...
}
// Avoid
async fn process_job(
db_ext: Extension<DB>,
path: Path<(String, Uuid)>,
query: Query<Pagination>,
) -> Result<Json<Job>> {
let Extension(db) = db_ext;
let Path((workspace, job_id)) = path;
// ...
}在函数参数中直接解构结构体:
rust
// Preferred
async fn process_job(
Extension(db): Extension<DB>,
Path((workspace, job_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Job>> {
// ...
}
// Avoid
async fn process_job(
db_ext: Extension<DB>,
path: Path<(String, Uuid)>,
query: Query<Pagination>,
) -> Result<Json<Job>> {
let Extension(db) = db_ext;
let Path((workspace, job_id)) = path;
// ...
}Trait Implementations
Trait实现
Use standard trait implementations to simplify conversions and reduce boilerplate:
rust
// Implement From/Into for type conversions
impl From<DbJob> for ApiJob {
fn from(db: DbJob) -> Self {
ApiJob {
id: db.id,
status: db.status.into(),
}
}
}
// Use TryFrom for fallible conversions
impl TryFrom<String> for JobKind {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> { ... }
}Apply macros to reduce boilerplate:
deriverust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job { ... }使用标准Trait实现来简化转换并减少样板代码:
rust
// Implement From/Into for type conversions
impl From<DbJob> for ApiJob {
fn from(db: DbJob) -> Self {
ApiJob {
id: db.id,
status: db.status.into(),
}
}
}
// Use TryFrom for fallible conversions
impl TryFrom<String> for JobKind {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> { ... }
}应用宏来减少样板代码:
deriverust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job { ... }Module Structure
模块结构
- Use instead of
pub(crate)when possible; expose only what needs exposingpub - Keep APIs small and expressive; avoid leaking internal types
- Organize code into modules reflecting ownership and domain boundaries
rust
// Prefer restricted visibility
pub(crate) fn internal_helper() { ... }
// Only pub for external API
pub fn create_job(...) -> Result<Job> { ... }- 尽可能使用而非
pub(crate);仅暴露需要对外公开的内容pub - 保持API小巧且表达性强;避免泄露内部类型
- 按照所有权和业务边界组织代码模块
rust
// Prefer restricted visibility
pub(crate) fn internal_helper() { ... }
// Only pub for external API
pub fn create_job(...) -> Result<Job> { ... }Code Navigation
代码导航
Always use rust-analyzer LSP for:
- Go to definition
- Find references
- Type information
- Import resolution
Do not guess at module paths or type definitions.
始终使用rust-analyzer LSP来:
- 跳转到定义
- 查找引用
- 查看类型信息
- 自动导入解析
不要猜测模块路径或类型定义。
JSON Handling
JSON处理
Prefer over when:
Box<serde_json::value::RawValue>serde_json::Value- Storing JSON in the database (JSONB columns)
- Passing JSON through without modification
- The JSON structure doesn't need inspection
rust
// Preferred - avoids parsing/serialization overhead
pub struct Job {
pub id: Uuid,
pub args: Option<Box<serde_json::value::RawValue>>,
}
// Only use Value when you need to inspect/modify JSON
let value: serde_json::Value = serde_json::from_str(&json)?;
if let Some(field) = value.get("field") {
// modify or inspect
}在以下场景中,优先使用而非:
Box<serde_json::value::RawValue>serde_json::Value- 在数据库中存储JSON(JSONB列)
- 直接传递JSON而不修改
- 无需检查JSON结构时
rust
// Preferred - avoids parsing/serialization overhead
pub struct Job {
pub id: Uuid,
pub args: Option<Box<serde_json::value::RawValue>>,
}
// Only use Value when you need to inspect/modify JSON
let value: serde_json::Value = serde_json::from_str(&json)?;
if let Some(field) = value.get("field") {
// modify or inspect
}Serde Optimizations
Serde优化
Use serde attributes to optimize serialization:
rust
#[derive(Serialize, Deserialize)]
pub struct Job {
#[serde(rename = "jobId")]
pub id: Uuid,
#[serde(default)]
pub priority: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}Prefer borrowing for zero-copy deserialization when lifetimes allow:
rust
#[derive(Deserialize)]
pub struct JobInput<'a> {
#[serde(borrow)]
pub workspace_id: Cow<'a, str>,
#[serde(borrow)]
pub script_path: &'a str,
}使用serde属性来优化序列化:
rust
#[derive(Serialize, Deserialize)]
pub struct Job {
#[serde(rename = "jobId")]
pub id: Uuid,
#[serde(default)]
pub priority: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}当生命周期允许时,优先使用借用实现零拷贝反序列化:
rust
#[derive(Deserialize)]
pub struct JobInput<'a> {
#[serde(borrow)]
pub workspace_id: Cow<'a, str>,
#[serde(borrow)]
pub script_path: &'a str,
}SQLx Patterns
SQLx模式
Never use - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version:
SELECT *rust
// Preferred - explicit columns
sqlx::query_as!(
Job,
"SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1",
job_id
)
// Avoid - breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id)Use batch operations to minimize round trips:
rust
// Preferred - single query with multiple values
sqlx::query!(
"INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)",
id1, log1, id2, log2
)
// Avoid N+1 queries
for id in ids {
sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?;
}
// Preferred - single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?Use transactions for multi-step operations and parameterize all queries.
绝对不要使用 - 始终显式列出列。当Worker版本落后于API服务器时,这对向后兼容性至关重要:
SELECT *rust
// Preferred - explicit columns
sqlx::query_as!(
Job,
"SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1",
job_id
)
// Avoid - breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id)使用批量操作来减少往返次数:
rust
// Preferred - single query with multiple values
sqlx::query!(
"INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)",
id1, log1, id2, log2
)
// Avoid N+1 queries
for id in ids {
sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?;
}
// Preferred - single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?对多步骤操作使用事务,并对所有查询进行参数化。
Async & Tokio Patterns
Async & Tokio模式
Never block the async runtime. Use for CPU-intensive or blocking I/O:
spawn_blockingrust
// Preferred - offload blocking work
let result = tokio::task::spawn_blocking(move || {
expensive_computation(&data)
}).await?;
// Avoid - blocks the runtime
let result = expensive_computation(&data); // Don't do this in asyncUse tokio primitives for sleep and channels:
rust
use tokio::sync::mpsc;
use tokio::time::sleep;
// Avoid in async contexts
use std::thread::sleep; // Blocks the runtimeUse bounded channels for backpressure:
rust
// Preferred - bounded channel prevents overwhelming
let (tx, rx) = tokio::sync::mpsc::channel(100);
// Be careful with unbounded
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();绝对不要阻塞异步运行时。对于CPU密集型或阻塞I/O操作,使用:
spawn_blockingrust
// Preferred - offload blocking work
let result = tokio::task::spawn_blocking(move || {
expensive_computation(&data)
}).await?;
// Avoid - blocks the runtime
let result = expensive_computation(&data); // Don't do this in async使用tokio原语来实现睡眠和通道:
rust
use tokio::sync::mpsc;
use tokio::time::sleep;
// Avoid in async contexts
use std::thread::sleep; // Blocks the runtime使用有界通道来实现背压:
rust
// Preferred - bounded channel prevents overwhelming
let (tx, rx) = tokio::sync::mpsc::channel(100);
// Be careful with unbounded
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();Mutex Selection in Async Code
异步代码中的Mutex选择
Prefer (or ) over for protecting data in async code. The async mutex is more expensive and only needed when holding locks across points.
std::sync::Mutexparking_lot::Mutextokio::sync::Mutex.awaitrust
// Preferred for data protection - std mutex is faster
use std::sync::Mutex;
struct Cache {
data: Mutex<HashMap<String, Value>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<Value> {
self.data.lock().unwrap().get(key).cloned()
}
fn insert(&self, key: String, value: Value) {
self.data.lock().unwrap().insert(key, value);
}
}Use only when you must hold the lock across points, typically for IO resources like database connections:
tokio::sync::Mutex.awaitrust
use tokio::sync::Mutex;
use std::sync::Arc;
// Async mutex for IO resources held across await points
let conn = Arc::new(Mutex::new(db_connection));
async fn execute_query(conn: Arc<Mutex<DbConn>>, query: &str) {
let mut lock = conn.lock().await;
lock.execute(query).await; // Lock held across .await
}Common pattern: Wrap in a struct with non-async methods that lock internally, keeping lock scope minimal:
Arc<Mutex<...>>rust
struct SharedState {
inner: std::sync::Mutex<StateInner>,
}
impl SharedState {
fn update(&self, value: i32) {
self.inner.lock().unwrap().value = value;
}
fn get(&self) -> i32 {
self.inner.lock().unwrap().value
}
}Alternative for IO resources: Spawn a dedicated task to manage the resource and communicate via message passing:
rust
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
handle_io_command(&mut resource, cmd).await;
}
});在异步代码中保护数据时,优先使用(或)而非。异步mutex开销更高,仅在需要跨点持有锁时才需要使用。
std::sync::Mutexparking_lot::Mutextokio::sync::Mutex.awaitrust
// Preferred for data protection - std mutex is faster
use std::sync::Mutex;
struct Cache {
data: Mutex<HashMap<String, Value>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<Value> {
self.data.lock().unwrap().get(key).cloned()
}
fn insert(&self, key: String, value: Value) {
self.data.lock().unwrap().insert(key, value);
}
}仅在必须跨点持有锁时使用,通常用于数据库连接等IO资源:
.awaittokio::sync::Mutexrust
use tokio::sync::Mutex;
use std::sync::Arc;
// Async mutex for IO resources held across await points
let conn = Arc::new(Mutex::new(db_connection));
async fn execute_query(conn: Arc<Mutex<DbConn>>, query: &str) {
let mut lock = conn.lock().await;
lock.execute(query).await; // Lock held across .await
}常见模式:将包装在结构体中,内部使用非异步方法来锁定,保持锁的作用域最小:
Arc<Mutex<...>>rust
struct SharedState {
inner: std::sync::Mutex<StateInner>,
}
impl SharedState {
fn update(&self, value: i32) {
self.inner.lock().unwrap().value = value;
}
fn get(&self) -> i32 {
self.inner.lock().unwrap().value
}
}IO资源替代方案:启动一个专门的任务来管理资源,并通过消息传递进行通信:
rust
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
handle_io_command(&mut resource, cmd).await;
}
});Build & Tooling
构建与工具
Build speed tips:
- Use during rapid iteration over
cargo checkcargo build - Minimize unnecessary dependencies and feature flags
构建速度优化技巧:
- 在快速迭代时使用而非
cargo checkcargo build - 最小化不必要的依赖和功能标志