Idiomatic Rust
The ten patterns that separate "I know the language" from "I write Rust". Iterator chains over loops; ? over match; newtypes over primitives; clippy as your style guide.
1 · Iterator chains over loops
// Less idiomatic
let mut squares = Vec::new();
for n in &nums {
if *n > 0 {
squares.push(n * n);
}
}
// More idiomatic
let squares: Vec<_> = nums.iter()
.filter(|n| **n > 0)
.map(|n| n * n)
.collect();Reads top-to-bottom as a pipeline. Same compiled output (LLVM is good at this), tighter source.
2 · ? everywhere; unwrap nowhere
// Outside tests, .unwrap() is a code smell.
let cfg = load_config()?; // good
let cfg = load_config().unwrap(); // bad
let cfg = load_config().expect("config must exist"); // ok at boundaries? propagates; expect("...") documents the assumption when you really must panic.
3 · Newtype over primitive
// Easy to mix up
fn transfer(from: u64, to: u64, amount: u64) { /* ... */ }
// Hard to misuse
struct UserId(u64);
struct Cents(u64);
fn transfer(from: UserId, to: UserId, amount: Cents) { /* ... */ }Compiler now rejects transfer(amount, sender, receiver) at the call site.
4 · Take &str, return String
Inputs that are read-only borrow (&str, &[T]); outputs
that the caller owns are returned by value (String, Vec<T>).
This lets callers pass owned or borrowed data without conversions.
5 · derive aggressively
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
id: u64,
name: String,
}Free implementations of the standard traits. Add Default if every field has a default; Serialize/Deserialize from serde for JSON.
6 · Builder pattern for many optional params
Request::new("/users")
.method(Method::POST)
.timeout_ms(500)
.body(payload)
.send()
.await?;Beats functions with 9 parameters. Crates like derive_builder generate the boilerplate.
7 · clippy is the style guide
$ cargo clippy --all-targets -- -D warningsTreat clippy lints as errors in CI. Many are pure style; many catch real bugs. Apply #[allow(clippy::lint_name)] only when you have a reason.
8 · Avoid unsafe unless you must
Unsafe Rust exists for FFI, low-level data structures, and performance work. For application code, you almost never need it. When you do, isolate to a single function and document the invariants.
9 · Trait bounds, not concrete types
// Less flexible
fn print_lines(s: String) { /* ... */ }
// More flexible
fn print_lines(s: impl AsRef<str>) { /* ... */ }
// Accepts &str, String, Cow<str>, etc.10 · When it really clicks
- You read compiler errors before reaching for Stack Overflow.
- You design the API by deciding the ownership and borrow shapes first.
- You're glad — not annoyed — when the borrow checker rejects something.