Rust Best Practices
Ownership and Borrowing
Prefer Borrowing Over Ownership
// Bad - takes ownership unnecessarily
fn print_name(name: String) {
println!("{}", name);
}
// Good - borrows immutably
fn print_name(name: &str) {
println!("{}", name);
}
Use References Appropriately
// Immutable borrow for reading
fn calculate_length(s: &String) -> usize {
s.len()
}
// Mutable borrow for modification
fn push_char(s: &mut String, c: char) {
s.push(c);
}
Error Handling
Use Result and Option
fn find_user(id: u32) -> Option<User> {
users.get(&id).cloned()
}
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?;
toml::from_str(&content).map_err(ConfigError::Parse)
}
Custom Error Types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {field}")]
Validation { field: String },
}
Propagate with ?
fn process_file(path: &str) -> Result<Data, Error> {
let content = fs::read_to_string(path)?;
let parsed = serde_json::from_str(&content)?;
let validated = validate(parsed)?;
Ok(validated)
}
Structs and Enums
Builder Pattern
#[derive(Default)]
pub struct ServerBuilder {
port: Option<u16>,
host: Option<String>,
}
impl ServerBuilder {
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn build(self) -> Result<Server, BuildError> {
Ok(Server {
port: self.port.unwrap_or(8080),
host: self.host.unwrap_or_else(|| "localhost".into()),
})
}
}
Newtype Pattern
pub struct UserId(u64);
pub struct Email(String);
impl Email {
pub fn new(email: String) -> Result<Self, ValidationError> {
if email.contains('@') {
Ok(Self(email))
} else {
Err(ValidationError::InvalidEmail)
}
}
}
Enums for State
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Error { message: String },
}
Traits
Implement Standard Traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct User {
pub id: u64,
pub name: String,
}
Trait Objects vs Generics
// Static dispatch (monomorphization)
fn process<T: Handler>(handler: T) { ... }
// Dynamic dispatch (trait object)
fn process(handler: &dyn Handler) { ... }
fn process(handler: Box<dyn Handler>) { ... }
Extension Traits
pub trait StringExt {
fn truncate_ellipsis(&self, max_len: usize) -> String;
}
impl StringExt for str {
fn truncate_ellipsis(&self, max_len: usize) -> String {
if self.len() <= max_len {
self.to_string()
} else {
format!("{}...", &self[..max_len - 3])
}
}
}
Lifetimes
Elision Rules
// Lifetimes elided - single input reference
fn first_word(s: &str) -> &str { ... }
// Explicit when multiple inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
Struct Lifetimes
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self { input, position: 0 }
}
}
Async Rust
Tokio Patterns
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = fetch_data().await?;
Ok(())
}
async fn fetch_data() -> Result<Data, Error> {
let client = reqwest::Client::new();
let response = client.get(URL).send().await?;
response.json().await.map_err(Into::into)
}
Concurrent Operations
use futures::future::join_all;
async fn fetch_all(urls: Vec<String>) -> Vec<Result<Response, Error>> {
let futures: Vec<_> = urls.into_iter()
.map(|url| fetch_one(url))
.collect();
join_all(futures).await
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic(expected = "division by zero")]
fn test_divide_by_zero() {
divide(1, 0);
}
}
Integration Tests
// tests/integration_test.rs
use mylib::process;
#[test]
fn test_full_workflow() {
let result = process("input");
assert!(result.is_ok());
}
Performance
Avoid Unnecessary Allocations
// Bad - allocates new String
fn process(s: &str) -> String {
s.to_uppercase()
}
// Good when possible - use Cow
use std::borrow::Cow;
fn process(s: &str) -> Cow<str> {
if s.chars().any(|c| c.is_lowercase()) {
Cow::Owned(s.to_uppercase())
} else {
Cow::Borrowed(s)
}
}
Use Iterators
// Good - lazy, zero-cost
let sum: i32 = numbers.iter()
.filter(|n| **n > 0)
.map(|n| n * 2)
.sum();
Anti-Patterns to Avoid
.unwrap() in production code
- Excessive
.clone() to "fix" borrow checker
unsafe without clear justification
- Ignoring compiler warnings
- Not using
clippy
- Global mutable state (
lazy_static with Mutex)