Languages
Rust
Ownership and borrowing in practice, error handling, async, and the cargo commands that matter day to day.
Cheatsheet #
| Task | Command |
|---|---|
| Check without codegen (fast) | cargo check --all-targets |
| Build optimised | cargo build --release |
| Run tests, show output | cargo test -- --nocapture |
| One test | cargo test path::to::test_name |
| Lint hard | cargo clippy --all-targets -- -D warnings |
| Format | cargo fmt --all (check: --check) |
| Explain an error code | rustc --explain E0502 |
| Expand a macro | cargo expand |
| Dependency tree | cargo tree -d (duplicates) |
| Audit advisories | cargo audit |
| Update within semver | cargo update |
| Add a dependency | cargo add serde --features derive |
| Benchmark | cargo bench (or criterion) |
| Docs for this crate and deps | cargo doc --open |
| Binary size breakdown | cargo bloat --release |
| MSRV / toolchain pin | rust-toolchain.toml |
Ownership in practice #
Every value has one owner; the value is dropped when the owner goes out of scope. You may have many shared references (&T) or exactly one mutable reference (&mut T), never both at once. The compiler enforces this at compile time, which is why there is no data race and no use-after-free.
let s = String::from("hello");
let t = s; // move: s is no longer usable
let u = t.clone(); // explicit copy when you really need two owners
fn read(v: &[u8]) {} // borrow: caller keeps ownership
fn consume(v: Vec<u8>) {} // take ownership: caller cannot use it afterwards
| Error | What it means | Usual fix |
|---|---|---|
E0382 use of moved value | You gave the value away | Borrow instead, or clone deliberately |
E0502 mutable borrow while borrowed | Two overlapping borrows | Shorten the first borrow’s scope, or restructure |
E0499 two mutable borrows | Aliasing mutation | Split the data (split_at_mut), or use indices |
E0597 does not live long enough | Reference outlives its owner | Own the data, or tie lifetimes explicitly |
E0308 mismatched types | Usually String vs &str, T vs &T | &x, x.as_str(), x.to_owned() |
Take &str and &[T] in function arguments, return String and Vec<T>. That gives callers the most freedom and avoids forcing an allocation to call you.
Errors #
Result<T, E> is the mechanism; ? propagates. Libraries define a concrete error type, applications flatten everything into one.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StoreError {
#[error("user {0} not found")]
NotFound(i64),
#[error("database: {0}")]
Db(#[from] sqlx::Error), // From impl, so ? converts automatically
}
pub fn get(id: i64) -> Result<User, StoreError> {
let row = query(id)?; // sqlx::Error converts via #[from]
row.ok_or(StoreError::NotFound(id))
}use anyhow::{Context, Result};
fn main() -> Result<()> {
let cfg = std::fs::read_to_string("config.toml")
.context("reading config.toml")?; // adds context, keeps the source
Ok(())
}thiserror for libraries (typed, matchable), anyhow for binaries (contextual, printable). unwrap() in production code is a deliberate assertion that the case is impossible — expect("reason") at least records why.
Options and iterators #
let name: Option<&str> = map.get("name").map(String::as_str);
let total: u64 = items.iter().filter(|i| i.active).map(|i| i.bytes).sum();
let first_err = results.iter().find_map(|r| r.as_ref().err());
let parsed: Result<Vec<i32>, _> = inputs.iter().map(|s| s.parse::<i32>()).collect(); // short-circuits
let (ok, bad): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
value.unwrap_or_default();
value.unwrap_or_else(|| expensive());
value.ok_or(Error::Missing)?;Iterators are lazy and compile to the same code as a hand-written loop; chain them freely. collect() into Result<Vec<_>, E> is the idiom for “all or nothing”.
Structs, traits and generics #
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Config {
pub name: String,
#[serde(default = "default_port")]
pub port: u16,
}
pub trait Store {
fn get(&self, id: i64) -> Result<User, StoreError>;
}
fn handler<S: Store>(store: &S) {} // static dispatch, monomorphised
fn handler_dyn(store: &dyn Store) {} // dynamic dispatch, one copy of the code
Use generics by default and dyn Trait when the type must be chosen at runtime or code size matters. impl Trait in argument position is shorthand for the generic form.
Shared state and concurrency #
| Need | Type |
|---|---|
| Single owner, single thread | Plain value |
| Shared, single thread | Rc<T> / Rc<RefCell<T>> |
| Shared across threads, read-only | Arc<T> |
| Shared and mutable across threads | Arc<Mutex<T>> or Arc<RwLock<T>> |
| Counter | AtomicU64 |
| Message passing | std::sync::mpsc or crossbeam/tokio::sync::mpsc |
let state = Arc::new(Mutex::new(HashMap::new()));
let s = Arc::clone(&state);
std::thread::spawn(move || {
s.lock().unwrap().insert("k", 1); // lock poisoned only if a holder panicked
});A MutexGuard held across an .await deadlocks an async runtime — use tokio::sync::Mutex there, or drop the guard before awaiting.
Async #
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = reqwest::Client::builder().timeout(Duration::from_secs(10)).build()?;
let (a, b) = tokio::try_join!(fetch(&client, "/a"), fetch(&client, "/b"))?;
let results = futures::future::join_all(ids.iter().map(|id| fetch_one(&client, *id))).await;
tokio::select! {
res = work() => res?,
_ = tokio::time::sleep(Duration::from_secs(5)) => anyhow::bail!("timeout"),
}
Ok(())
}Futures do nothing until awaited. Blocking calls (file I/O, std::thread::sleep, CPU-bound work) inside an async task stall the whole worker thread — move them to tokio::task::spawn_blocking.
Tests #
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_input() {
assert_eq!(parse("a=1").unwrap(), Config { name: "a".into(), port: 1 });
}
#[test]
fn rejects_empty() {
assert!(matches!(parse(""), Err(ParseError::Empty)));
}
#[tokio::test]
async fn fetches() { /* ... */ }
}Unit tests live beside the code in mod tests; integration tests live in tests/ and can only use the public API — which makes them the honest check of your interface.
Cargo and builds #
[profile.release]
lto = "thin"
codegen-units = 1
panic = "abort" # smaller and faster, but no unwinding and no catch_unwind
strip = "symbols"cargo build --release --target x86_64-unknown-linux-musl # static binary for a scratch image
cargo test --workspace --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo tree -i openssl # who pulls this in
cargo update -p serde --precise 1.0.203Commit Cargo.lock for binaries, omit it for libraries. Pin the toolchain with rust-toolchain.toml so CI and laptops agree.
Oneliners #
# Explain the error you just got
rustc --explain E0502
# Fastest feedback loop
cargo watch -x check -x test
# Show where time goes in a slow build
cargo build --release --timings && open target/cargo-timings/cargo-timing.html
# Duplicate dependency versions bloating the build
cargo tree -d
# Unused dependencies
cargo +nightly udeps
# Which features are enabled for a crate
cargo tree -e features -i tokio
# Expand a derive or macro to see what it generates
cargo expand --lib path::to::module
# Static musl binary, no glibc
cargo build --release --target x86_64-unknown-linux-musl && ldd target/x86_64-unknown-linux-musl/release/app
# Check the whole workspace without building artefacts
cargo check --workspace --all-targets --all-features
# Fail CI on formatting
cargo fmt --all -- --check
# Security advisories in the lock file
cargo audit --deny warnings
# Miri for undefined behaviour in unsafe code
cargo +nightly miri test