Rust Best Practices
This guide outlines our team's definitive best practices for writing Rust code. Adhere to these principles to ensure consistency, performance, and maintainability across all projects.
1. Code Organization and Structure
1.1 Module Structure: Feature-Driven, Not File-Type-Driven
Organize modules by feature or functionality, not by separating types from implementations. Keep related structs, enums, and their impl blocks together within the same module.
❌ BAD:
// types.rs
pub struct User { pub id: u64 }
// impls.rs
impl User {
pub fn new(id: u64) -> Self { Self { id } }
}
✅ GOOD:
// user.rs (or a user/mod.rs)
pub struct User { pub id: u64 }
impl User {
pub fn new(id: u64) -> Self { Self { id } }
pub fn get_id(&self) -> u64 { self.id }
}
1.2 Small, Cohesive Structs
Design structs to be small and focused. Break down complex data into multiple smaller, composable structs. This improves borrow-checker ergonomics and API flexibility.
❌ BAD:
pub struct OrderProcessor {
pub order_id: u64, pub customer_id: u64, pub items: Vec<String>,
pub total_amount: f64, pub payment_status: String,
pub shipping_address_street: String, pub shipping_address_city: String,
}
✅ GOOD:
pub struct Order {
pub id: u64, pub customer_id: u64, pub items: Vec<String>,
pub total_amount: f64, pub payment_status: String,
}
pub struct Address { pub street: String, pub city: String }
pub struct OrderProcessor {
pub order: Order,
pub shipping_address: Address,
}
2. Common Patterns and Anti-patterns
2.1 Newtype Pattern for Type Safety
Wrap primitive types in newtype structs to enforce strong typing and prevent logical errors from mixing incompatible IDs or values.
❌ BAD:
fn process_transaction(user_id: u64, account_id: u64) { /* ... */ }
// Calling with swapped arguments compiles: process_transaction(account.id, user.id);
✅ GOOD:
#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct UserId(u64);
#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct AccountId(u64);
fn process_transaction(user_id: UserId, account_id: AccountId) { /* ... */ }
// Compiler prevents: process_transaction(account.id, user.id);
2.2 Builder Pattern for Complex Construction
Use the Builder pattern for structs with many optional fields or complex initialization logic. Avoid it for simple structs with one or two clear constructors.
❌ BAD (for many fields):
pub struct Config { pub timeout: u64, pub retries: u8 }
impl Config {
pub fn new(timeout: Option<u64>, retries: Option<u8>) -> Self { /* ... */ }
}
✅ GOOD:
pub struct Config { pub timeout: u64, pub retries: u8 }
impl Config { pub fn builder() -> ConfigBuilder { ConfigBuilder::default() } }
#[derive(Default)] pub struct ConfigBuilder { pub timeout: Option<u64>, pub retries: Option<u8> }
impl ConfigBuilder {
pub fn timeout(mut self, timeout: u64) -> Self { self.timeout = Some(timeout); self }
pub fn retries(mut self, retries: u8) -> Self { self.retries = Some(retries); self }
pub fn build(self) -> Result<Config, &'static str> {
Ok(Config { timeout: self.timeout.unwrap_or(1000), retries: self.retries.unwrap_or(3) })
}
}
2.3 Minimal Generic Bounds on Types
Apply generic bounds only when they are intrinsic to the type definition. Place stricter, derivable bounds on impl blocks or specific functions, not on the struct/enum itself.
❌ BAD:
pub struct Cache<K: std::hash::Hash + Eq + std::fmt::Debug, V: Clone> { /* ... */ }
✅ GOOD:
pub struct Cache<K, V> { /* ... */ } // K and V are just types
impl<K: std::hash::Hash + Eq, V> Cache<K, V> {
pub fn new() -> Self { /* ... */ }
}
impl<K: std::fmt::Debug, V: std::fmt::Debug> Cache<K, V> {
pub fn print_keys(&self) { /* ... */ }
}
3. Performance Considerations
3.1 Default to Vec and HashMap
Start with Vec for sequences and HashMap for maps. They are highly optimized and cover most use cases. Only switch to other collections (VecDeque, BTreeMap, LinkedList, etc.) when profiling shows a specific performance bottleneck or a unique functional requirement (e.g., sorted keys for BTreeMap).
❌ BAD:
use std::collections::LinkedList; // Unnecessary for most simple lists
fn process_items(items: LinkedList<String>) { /* ... */ }
✅ GOOD:
use std::collections::HashMap; // Default for key-value pairs
fn process_items(items: Vec<String>) { /* ... */ }
fn lookup_data(data: HashMap<String, u32>) { /* ... */ }
3.2 Pre-allocate Collection Capacity
When the approximate size of a collection is known beforehand, pre-allocate its capacity to avoid frequent reallocations, which can be costly.
❌ BAD:
let mut vec = Vec::new();
for i in 0..1000 { vec.push(i); } // Potentially many reallocations
✅ GOOD:
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 { vec.push(i); } // Single allocation
4. Common Pitfalls and Gotchas
4.1 Error Handling: Result for Recoverable Errors
Always use Result<T, E> for errors that can be handled or propagated. Reserve panic! for unrecoverable bugs or situations indicating a programming error.
❌ BAD:
fn load_config(path: &str) -> String {
std::fs::read_to_string(path).expect("Failed to read config")
}
✅ GOOD:
fn load_config(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
4.2 Document unsafe Blocks
Every unsafe block must be preceded by a // SAFETY: comment explaining why the code inside is sound and upholds Rust's safety invariants.
❌ BAD:
let mut vec = vec![0];
let ptr = vec.as_mut_ptr();
unsafe { *ptr = 42; } // Missing safety justification
✅ GOOD:
let mut vec = vec![0];
let ptr = vec.as_mut_ptr();
// SAFETY: `ptr` is a valid pointer to `vec[0]`, and we are writing a valid
// integer `42` to it. The vector has at least one element.
unsafe { *ptr = 42; }
5. Testing Approaches
5.1 Unit Tests with #[test]
Use #[test] for isolated unit tests, typically placed in a tests module within the same file as the code being tested.
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
}
5.2 Example Tests with rustdoc
Leverage rustdoc for documentation examples. These examples are compiled and run as tests, ensuring your documentation stays up-to-date and correct.
/// Adds two integers together.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::add(5, 3), 8);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}