Rust Guide¶
Applies to: Rust 1.70+, Systems Programming, WebAssembly, CLIs
Core Principles¶
- Memory Safety: No null, no dangling pointers, no data races
- Ownership: Clear ownership, borrowing, and lifetimes
- Zero-Cost Abstractions: High-level code with low-level performance
- Fearless Concurrency: Compiler prevents data races
- Explicit Over Implicit: Types, errors, and behavior are explicit
Language-Specific Guardrails¶
Rust Version & Setup¶
✓ Use Rust 1.70+ (stable channel)
✓ Use Cargo.toml for dependency management
✓ Pin dependency versions or use ~ for compatible updates
✓ Run cargo update periodically for security patches
Code Style (Rustfmt)¶
✓ Run cargo fmt before every commit
✓ Run cargo clippy for linting
✓ Follow Rust API Guidelines
✓ Use snake_case for functions, variables, modules
✓ Use PascalCase for types, traits, enums
✓ Use SCREAMING_SNAKE_CASE for constants
✓ Prefer exhaustive match over if let chains
Error Handling¶
✓ Use Result<T, E> for recoverable errors
✓ Use custom error types (not just String)
✓ Implement std::error::Error trait for custom errors
✓ Use ? operator for error propagation
✓ Only use panic! or unwrap() when truly impossible to fail
✓ Use expect() with message over unwrap() when confident
Ownership & Borrowing¶
✓ Prefer borrowing (&T) over owned (T) in function parameters
✓ Use &mut T only when mutation needed
✓ Avoid clone() unless necessary (understand cost)
✓ Use Cow<'a, T> for clone-on-write scenarios
✓ Lifetime annotations only when compiler can't infer
Safety¶
✓ Minimize unsafe blocks (document invariants)
✓ Use #[forbid(unsafe_code)] in crates that shouldn't have unsafe
✓ Audit all unsafe in code reviews
✓ Prefer safe abstractions (Vec<T> over raw pointers)
Error Handling Patterns¶
Using thiserror (Libraries)¶
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataError {
#[error("User {id} not found")]
NotFound { id: String },
#[error("Invalid email format: {0}")]
InvalidEmail(String),
#[error(transparent)]
Database(#[from] sqlx::Error),
}
Using anyhow (Applications)¶
use anyhow::{Context, Result};
fn read_config() -> Result<Config> {
let contents = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
toml::from_str(&contents)
.context("Failed to parse config")
}
Testing¶
Guardrails¶
✓ Tests in same file: #[cfg(test)] mod tests { ... }
✓ Integration tests in tests/ directory
✓ Public API in lib.rs, private in modules
✓ Coverage target: >80% for libraries, >60% for applications
✓ Use cargo tarpaulin or cargo llvm-cov for coverage
Example¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_divide_by_zero() {
divide(10, 0);
}
#[test]
fn test_result() -> Result<(), Box<dyn std::error::Error>> {
let result = fallible_operation()?;
assert_eq!(result, 42);
Ok(())
}
}
Tooling¶
Essential Commands¶
# Format
cargo fmt
# Lint
cargo clippy -- -D warnings
# Test
cargo test
cargo test --all-features
# Build
cargo build --release
# Check (faster than build)
cargo check
# Documentation
cargo doc --open
# Audit dependencies
cargo audit
# Benchmarks
cargo bench
Configuration¶
# Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
rust-version = "1.70" # MSRV
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
[profile.release]
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
opt-level = 3 # Maximum optimization
strip = true # Strip symbols
Common Pitfalls¶
Don't Do This¶
// ❌ Unnecessary clone
fn process(data: String) {
println!("{}", data.clone());
}
// ❌ Unwrap without justification
let value = risky_operation().unwrap();
// ❌ Ignoring Result
let _ = File::create("file.txt");
// ❌ String for errors
fn do_something() -> Result<(), String> {
Err("something went wrong".to_string())
}
Do This Instead¶
// ✅ Borrow instead of clone
fn process(data: &str) {
println!("{}", data);
}
// ✅ Proper error handling
let value = risky_operation()
.expect("Operation is guaranteed to succeed because...");
// ✅ Handle Result
File::create("file.txt").context("Failed to create file")?;
// ✅ Custom error type
fn do_something() -> Result<(), AppError> {
Err(AppError::OperationFailed)
}
Async Patterns (Tokio)¶
Basic Async Function¶
use tokio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = fetch_data().await?;
println!("Result: {}", result);
Ok(())
}
async fn fetch_data() -> Result<String, reqwest::Error> {
let response = reqwest::get("https://api.example.com/data")
.await?
.text()
.await?;
Ok(response)
}
Concurrent Tasks¶
use tokio::task;
async fn process_concurrently() -> Result<(), AppError> {
let task1 = task::spawn(async { fetch_users().await });
let task2 = task::spawn(async { fetch_posts().await });
let (users, posts) = tokio::try_join!(task1, task2)?;
Ok(())
}
Timeouts¶
use tokio::time::{timeout, Duration};
async fn with_timeout() -> Result<Data, AppError> {
let result = timeout(
Duration::from_secs(5),
fetch_data()
).await
.map_err(|_| AppError::Timeout)?;
result
}
Web Server (Axum)¶
use axum::{
routing::{get, post},
Json, Router,
extract::{Path, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: Arc<Database>,
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(Database::new()),
};
let app = Router::new()
.route("/health", get(health))
.route("/users", post(create_user))
.route("/users/:id", get(get_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn health() -> &'static str {
"OK"
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<Json<User>, StatusCode> {
let user = state.db.get_user(id)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
Performance Considerations¶
Optimization Guardrails¶
✓ Use Vec::with_capacity() when size known
✓ Use iterators over manual loops (zero-cost abstractions)
✓ Profile with cargo flamegraph before optimizing
✓ Benchmark with criterion crate
✓ Use &str over String when possible
✓ Consider Arc<T> vs Rc<T> for sharing (Arc is thread-safe)
Example¶
// Pre-allocate capacity
let mut vec = Vec::with_capacity(1000);
// Iterator chains (zero-cost)
let sum: i32 = numbers
.iter()
.filter(|&&x| x > 0)
.map(|x| x * 2)
.sum();
// String slices over owned strings
fn process(text: &str) { // Not String
// ...
}
Security Best Practices¶
Guardrails¶
✓ Use #[forbid(unsafe_code)] unless unsafe truly needed
✓ Validate all external inputs (serde, custom validators)
✓ Use secrecy crate for sensitive data (API keys, passwords)
✓ Hash passwords with argon2 or bcrypt
✓ Use ring or rustls for cryptography
✓ Run cargo audit regularly
✓ Use cargo deny to check licenses and security
Example¶
use secrecy::{Secret, ExposeSecret};
struct Config {
api_key: Secret<String>,
}
fn use_api_key(config: &Config) {
let key = config.api_key.expose_secret();
// Use key (not logged, not displayed by Debug)
}