rust-backend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rust Backend Coding Guidelines

Rust后端编码规范

Apply these patterns when writing or modifying Rust code in the
backend/
directory.
backend/
目录中编写或修改Rust代码时,请遵循以下模式。

Data Structure Design

数据结构设计

Choose between
struct
,
enum
, or
newtype
based on domain needs:
  • Use
    enum
    for state machines instead of boolean flags or loosely related fields
  • Model invariants explicitly using types (e.g.,
    NonZeroU32
    ,
    Duration
    , custom enums)
  • Consider ownership of each field:
    • Use
      &str
      vs
      String
      , slices vs vectors
    • Use
      Arc<T>
      when sharing across threads
    • Use
      Cow<'a, T>
      for flexible ownership
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,
}
根据业务需求选择
struct
enum
newtype
  • 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
impl
blocks immediately below the struct/enum they modify. Group methods logically:
rust
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> { ... }
}
impl
块紧跟在其对应的struct/enum下方。按逻辑分组方法:
rust
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 (
.filter().map().collect()
) over imperative for-loops:
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());
    }
}
优先使用函数式迭代器链(
.filter().map().collect()
)而非命令式for循环:
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
Error
type from
windmill_common::error
. Return
Result<T, Error>
or
JsonResult<T>
for fallible functions:
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
if let
for optional handling. Use
let...else
when early return makes code clearer:
rust
let Some(config) = get_config() else {
    return Err(Error::MissingConfig);
};
Never panic in library code. Reserve
.unwrap()
for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity.
使用
windmill_common::error
中的
Error
类型。对于可能失败的函数,返回
Result<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 let
处理可选值。当提前返回能让代码更清晰时,使用
let...else
rust
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
derive
macros to reduce boilerplate:
rust
#[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> { ... }
}
应用
derive
宏来减少样板代码:
rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job { ... }

Module Structure

模块结构

  • Use
    pub(crate)
    instead of
    pub
    when possible; expose only what needs exposing
  • 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
Box<serde_json::value::RawValue>
over
serde_json::Value
when:
  • 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
SELECT *
- always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version:
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.
绝对不要使用
SELECT *
- 始终显式列出列。当Worker版本落后于API服务器时,这对向后兼容性至关重要:
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
spawn_blocking
for CPU-intensive or blocking I/O:
rust
// 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
Use 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 runtime
Use 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_blocking
rust
// 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
std::sync::Mutex
(or
parking_lot::Mutex
) over
tokio::sync::Mutex
for protecting data in async code. The async mutex is more expensive and only needed when holding locks across
.await
points.
rust
// 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
tokio::sync::Mutex
only when you must hold the lock across
.await
points
, typically for IO resources like database connections:
rust
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
Arc<Mutex<...>>
in a struct with non-async methods that lock internally, keeping lock scope minimal:
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;
    }
});
在异步代码中保护数据时,优先使用
std::sync::Mutex
(或
parking_lot::Mutex
)而非
tokio::sync::Mutex
。异步mutex开销更高,仅在需要跨
.await
点持有锁时才需要使用。
rust
// 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);
    }
}
仅在必须跨
.await
点持有锁时使用
tokio::sync::Mutex
,通常用于数据库连接等IO资源:
rust
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
    cargo check
    during rapid iteration over
    cargo build
  • Minimize unnecessary dependencies and feature flags
构建速度优化技巧:
  • 在快速迭代时使用
    cargo check
    而非
    cargo build
  • 最小化不必要的依赖和功能标志