Rust is a systems programming language focused on safety, speed, and concurrency — without a garbage collector. This cheat sheet covers the full Rust language from variables to async, with copy-ready code for everyday development.
Quick reference
The 25 patterns that cover 95% of everyday Rust development.
| Pattern | Example |
|---|---|
| Variable binding | let x = 5; |
| Mutable variable | let mut x = 5; x += 1; |
| Type annotation | let x: i32 = 5; |
| Constant | const MAX: u32 = 100; |
| Function | fn add(a: i32, b: i32) -> i32 { a + b } |
| Struct | struct Point { x: f64, y: f64 } |
| Enum | enum Direction { North, South, East, West } |
| Pattern match | match x { 1 => "one", _ => "other" } |
| Option | let v: Option<i32> = Some(42); |
| Result | let r: Result<i32, String> = Ok(42); |
? operator |
let n = str.parse::<i32>()?; |
| Borrow | let r = &x; |
| Mutable borrow | let r = &mut x; |
| Clone | let y = x.clone(); |
| Vec | let v: Vec<i32> = vec![1, 2, 3]; |
| HashMap | let mut m = HashMap::new(); |
| Iterator map | v.iter().map(|x| x * 2).collect() |
| Closure | let add = |a, b| a + b; |
| Trait impl | impl Display for Point { ... } |
| Generic function | fn first<T>(v: &[T]) -> &T { &v[0] } |
| Lifetime | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str |
| Trait object | Box<dyn Animal> |
| Thread | thread::spawn(|| { ... }); |
| Channel | let (tx, rx) = mpsc::channel(); |
| Async function | async fn fetch() -> Result<String, Error> |
Variables and types
// Immutable by default
let x = 5;
let y: f64 = 3.14;
let s = String::from("hello");
let b = true;
// Mutable
let mut count = 0;
count += 1;
// Shadowing (new binding, can change type)
let x = x + 1; // x is now 6
let x = x.to_string(); // x is now "6"
// Constants (must have type, computed at compile time)
const MAX_POINTS: u32 = 100_000;
static GREETING: &str = "hello";
// Destructuring
let (a, b, c) = (1, 2, 3);
let Point { x, y } = point;
let [first, .., last] = [1, 2, 3, 4, 5];
Scalar types
| Type | Description | Example |
|---|---|---|
i8 – i128, isize |
Signed integers | let n: i32 = -42; |
u8 – u128, usize |
Unsigned integers | let n: u8 = 255; |
f32, f64 |
Floating point | let f: f64 = 3.14; |
bool |
Boolean | let b = true; |
char |
Unicode scalar (4 bytes) | let c = '😀'; |
Compound types
// Tuple
let tup: (i32, f64, char) = (42, 3.14, 'z');
let (x, y, z) = tup;
let first = tup.0;
// Array (fixed size, stack allocated)
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let len = arr.len();
Strings
// &str — string slice (borrowed, static or from String)
let s1: &str = "hello world";
let word = &s1[0..5]; // "hello"
// String — heap-allocated, growable
let mut s2 = String::new();
let s3 = String::from("hello");
let s4 = "hello".to_string();
// Concatenation
let s5 = s3 + " world"; // s3 moved here
let s6 = format!("{} {}", "hi", "there"); // no move
// Useful methods
s2.push_str("hello");
s2.push('!');
let len = s2.len(); // bytes, not chars
let chars: Vec<char> = s2.chars().collect();
let contains = s2.contains("ell");
let replaced = s2.replace("hello", "hi");
let upper = s2.to_uppercase();
let trimmed = " hi ".trim();
let parts: Vec<&str> = "a,b,c".split(',').collect();
// &str vs String conversion
let owned: String = s1.to_string();
let borrowed: &str = &s5;
// Raw strings (no escape sequences needed)
let raw = r#"C:\Users\name\file.txt"#;
let raw2 = r"no \n escaping here";
Control flow
// if / else if / else
if x < 0 {
println!("negative");
} else if x == 0 {
println!("zero");
} else {
println!("positive");
}
// if as expression
let abs = if x < 0 { -x } else { x };
// loop (infinite, can return value)
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
// while
while n < 100 {
n *= 2;
}
// for with range
for i in 0..5 { print!("{i} "); } // 0 1 2 3 4
for i in 0..=5 { print!("{i} "); } // 0 1 2 3 4 5
// for over collection
for item in &vec {
println!("{item}");
}
// enumerate
for (i, val) in vec.iter().enumerate() {
println!("{i}: {val}");
}
// match
let msg = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "F",
};
// match with binding
match point {
Point { x: 0, y } => println!("on y-axis at {y}"),
Point { x, y: 0 } => println!("on x-axis at {x}"),
Point { x, y } => println!("at ({x}, {y})"),
}
// if let (one pattern match)
if let Some(val) = maybe_value {
println!("got {val}");
}
// while let
while let Some(top) = stack.pop() {
println!("{top}");
}
Functions and closures
// Basic function
fn greet(name: &str) -> String {
format!("Hello, {name}!") // last expression is return value
}
// Multiple return via tuple
fn min_max(v: &[i32]) -> (i32, i32) {
(*v.iter().min().unwrap(), *v.iter().max().unwrap())
}
// Closures
let double = |x: i32| x * 2;
let add = |a, b| a + b;
let square = |x| x * x;
// Capturing environment
let offset = 10;
let add_offset = |x| x + offset; // borrows offset
// Moving into closure
let data = vec![1, 2, 3];
let owns_data = move || println!("{:?}", data); // data moved in
// Higher-order functions
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
apply(|x| x * 3, 5); // 15
// Returning closures (must box)
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
Ownership and borrowing
This is Rust's core — enforced at compile time, zero runtime cost.
// Move semantics: value has one owner
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2, s1 no longer valid
// println!("{s1}"); // ERROR: use of moved value
// Clone: explicit deep copy
let s3 = s2.clone(); // both s2 and s3 are valid
// Copy types (stack-only, implicit copy)
let x = 5;
let y = x; // x is still valid (integers are Copy)
// Borrowing: shared reference (&T)
fn print_len(s: &String) {
println!("Length: {}", s.len());
}
let s = String::from("hello");
print_len(&s); // borrow, not move — s still valid
// Mutable reference (&mut T)
fn add_exclaim(s: &mut String) {
s.push('!');
}
let mut s = String::from("hello");
add_exclaim(&mut s); // s is now "hello!"
// Rules:
// 1. Any number of shared (&T) references OR exactly one mutable (&mut T)
// 2. References must always be valid (no dangling pointers)
// Slices (reference to a contiguous sequence)
let v = vec![1, 2, 3, 4, 5];
let slice: &[i32] = &v[1..3]; // [2, 3]
let s = String::from("hello world");
let word: &str = &s[0..5]; // "hello"
Structs
// Named-field struct
#[derive(Debug, Clone)]
struct User {
username: String,
email: String,
active: bool,
login_count: u64,
}
// Constructor pattern
impl User {
fn new(username: &str, email: &str) -> Self {
Self {
username: username.to_string(),
email: email.to_string(),
active: true,
login_count: 0,
}
}
}
// Instantiation and struct update syntax
let user1 = User::new("alice", "alice@example.com");
let user2 = User {
email: String::from("bob@example.com"),
..user1 // rest from user1 (moves non-Copy fields)
};
// Methods (impl block)
impl User {
fn is_active(&self) -> bool {
self.active
}
fn deactivate(&mut self) {
self.active = false;
}
fn into_email(self) -> String { // consumes self
self.email
}
}
// Tuple struct
struct Point(f64, f64);
let p = Point(1.0, 2.0);
let x = p.0;
// Unit struct (for traits)
struct Marker;
Enums and pattern matching
// Basic enum
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
}
// Enum with data
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
// Option<T>: Some(value) or None
let maybe: Option<i32> = Some(42);
let nothing: Option<i32> = None;
let doubled = maybe.map(|x| x * 2); // Some(84)
let value = maybe.unwrap_or(0); // 42
let value = maybe.unwrap_or_default(); // 42
let value = maybe.unwrap_or_else(|| compute()); // lazy
if let Some(v) = maybe {
println!("{v}");
}
// Result<T, E>: Ok(value) or Err(error)
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("oops".to_string());
let value = ok.unwrap_or(0);
let mapped = ok.map(|v| v * 2);
let chained = ok.and_then(|v| if v > 0 { Ok(v) } else { Err("negative".to_string()) });
// ? operator — propagates Err early
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse::<i32>()?; // returns Err if fails
Ok(n * 2)
}
Traits
Traits define shared behaviour — Rust's alternative to interfaces.
// Define a trait
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> &str;
// Default implementation
fn describe(&self) -> String {
format!("{} says {}", self.name(), self.sound())
}
}
// Implement trait for a type
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> &str { "woof" }
}
struct Cat;
impl Animal for Cat {
fn name(&self) -> &str { "cat" }
fn sound(&self) -> &str { "meow" }
}
// Trait bounds in functions
fn print_sound(animal: &impl Animal) { // impl Trait syntax
println!("{}", animal.describe());
}
// Generic with where clause
fn loudest<T>(a: T, b: T) -> T
where
T: Animal + PartialOrd,
{
if a.name() > b.name() { a } else { b }
}
// Trait objects (dynamic dispatch)
fn make_noise(animals: &[Box<dyn Animal>]) {
for a in animals {
println!("{}", a.describe());
}
}
// Common standard traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Point { x: i32, y: i32 }
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
use std::ops::Add;
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
Generics
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
// Generic struct
struct Pair<T> {
first: T,
second: T,
}
impl<T: fmt::Display + PartialOrd> Pair<T> {
fn new(first: T, second: T) -> Self { Self { first, second } }
fn cmp_display(&self) {
if self.first >= self.second {
println!("first: {}", self.first);
} else {
println!("second: {}", self.second);
}
}
}
// Multiple trait bounds
fn print_all<T: fmt::Debug + fmt::Display>(items: &[T]) {
for item in items {
println!("{item}");
}
}
// Generic enum (like the standard library's Option and Result)
enum MyOption<T> { Some(T), None }
enum MyResult<T, E> { Ok(T), Err(E) }
Lifetimes
Lifetimes tell the borrow checker how long references are valid.
// Lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Lifetime in struct (struct cannot outlive the reference it holds)
struct Important<'a> {
part: &'a str,
}
impl<'a> Important<'a> {
fn level(&self) -> &str { self.part }
}
// Static lifetime — lives for the entire program
let s: &'static str = "I live forever";
// Lifetime elision rules (compiler infers in common cases)
fn first_word(s: &str) -> &str { // 'a inferred
s.split_whitespace().next().unwrap_or("")
}
Collections
use std::collections::{HashMap, HashSet, BTreeMap, VecDeque};
// Vec<T>
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3];
v.push(4);
v.pop(); // Option<i32>
v.insert(0, 10);
v.remove(0); // removes and returns element
let len = v.len();
let cap = v.capacity();
v.sort();
v.sort_by(|a, b| b.cmp(a)); // reverse sort
v.dedup(); // remove consecutive duplicates
let contains = v.contains(&3);
v.retain(|&x| x > 0); // keep only positives
// Slices from Vec
let slice: &[i32] = &v[1..3];
// HashMap<K, V>
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
// Entry API (insert if missing)
scores.entry("Charlie".to_string()).or_insert(0);
*scores.entry("Alice".to_string()).or_insert(0) += 10;
// Access
let alice = scores.get("Alice"); // Option<&i32>
let alice = scores["Alice"]; // panics if missing
let alice = scores.get("Alice").copied().unwrap_or(0);
// Iteration
for (name, score) in &scores {
println!("{name}: {score}");
}
// HashSet<T>
let mut set: HashSet<i32> = HashSet::new();
set.insert(1); set.insert(2); set.insert(3);
set.contains(&2);
let union: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();
let diff: HashSet<_> = a.difference(&b).collect();
// VecDeque<T> — double-ended queue
let mut dq: VecDeque<i32> = VecDeque::new();
dq.push_back(1);
dq.push_front(0);
dq.pop_front();
dq.pop_back();
Iterators
Iterators are lazy — no computation until consumed.
let v = vec![1, 2, 3, 4, 5];
// Adapters (lazy, return new iterators)
v.iter().map(|x| x * 2)
v.iter().filter(|&&x| x > 2)
v.iter().filter_map(|x| if *x > 2 { Some(x * 10) } else { None })
v.iter().take(3)
v.iter().skip(2)
v.iter().enumerate() // (index, value)
v.iter().zip(other.iter()) // (a, b) pairs
v.iter().flat_map(|x| [x, x]) // flatten one level
v.iter().chain(other.iter()) // concatenate
v.iter().peekable() // peek without consuming
v.iter().rev() // reverse
// Consumers (eager, return a value)
v.iter().collect::<Vec<_>>()
v.iter().sum::<i32>() // 15
v.iter().product::<i32>() // 120
v.iter().count() // 5
v.iter().min() // Some(1)
v.iter().max() // Some(5)
v.iter().any(|&x| x > 4) // true
v.iter().all(|&x| x > 0) // true
v.iter().find(|&&x| x > 3) // Some(4)
v.iter().position(|&x| x == 3) // Some(2)
v.iter().fold(0, |acc, x| acc + x) // 15
v.iter().for_each(|x| print!("{x} "));
// Chaining example
let result: Vec<String> = (1..=10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.map(|x| format!("{x}"))
.collect();
// ["4", "16", "36", "64", "100"]
// iter() vs into_iter() vs iter_mut()
v.iter() // &T — borrows elements
v.iter_mut() // &mut T — mutable borrows
v.into_iter() // T — consumes v, yields owned values
// Custom iterator
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Error handling
use std::fmt;
use std::num::ParseIntError;
// Custom error type
#[derive(Debug)]
enum AppError {
ParseError(ParseIntError),
TooLarge(i32),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::ParseError(e) => write!(f, "parse error: {e}"),
AppError::TooLarge(n) => write!(f, "{n} is too large"),
AppError::NotFound(key) => write!(f, "{key} not found"),
}
}
}
impl std::error::Error for AppError {}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::ParseError(e) }
}
// Using ? with custom error (From conversion is automatic)
fn parse_and_validate(s: &str) -> Result<i32, AppError> {
let n = s.parse::<i32>()?; // ParseIntError → AppError via From
if n > 1000 {
return Err(AppError::TooLarge(n));
}
Ok(n)
}
// thiserror crate (simplify custom errors)
use thiserror::Error;
#[derive(Debug, Error)]
enum MyError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(#[from] ParseIntError),
#[error("value {0} out of range")]
OutOfRange(i32),
}
// anyhow crate (quick scripts and applications)
use anyhow::{Context, Result};
fn run() -> Result<()> {
let content = std::fs::read_to_string("file.txt")
.context("failed to read file")?;
let n: i32 = content.trim().parse()
.context("file content is not a number")?;
println!("parsed: {n}");
Ok(())
}
// Panic (reserve for truly unrecoverable situations)
let v = vec![1, 2, 3];
v[99]; // index out of bounds → panic
panic!("unrecoverable error"); // explicit panic
Modules and crates
// src/main.rs or src/lib.rs
mod math {
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn subtract(a: i32, b: i32) -> i32 { a - b }
pub mod advanced {
pub fn power(base: i32, exp: u32) -> i32 {
base.pow(exp)
}
}
}
// Using modules
use math::add;
use math::advanced::power;
math::subtract(5, 3);
// File-based modules (src/math.rs or src/math/mod.rs)
mod math; // loads src/math.rs
// Re-exporting
pub use math::add; // clients can access crate::add directly
// Cargo.toml dependencies
// [dependencies]
// serde = { version = "1", features = ["derive"] }
// tokio = { version = "1", features = ["full"] }
// thiserror = "1"
// anyhow = "1"
// use shorthand
use std::{
collections::{HashMap, HashSet},
fmt::{self, Display},
io::{self, Read, Write},
};
Concurrency
use std::thread;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::mpsc;
// Spawn a thread
let handle = thread::spawn(|| {
println!("from thread");
42 // return value
});
let result = handle.join().unwrap(); // 42
// Move data into thread (move closure)
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", data);
});
handle.join().unwrap();
// Mutex<T> — mutual exclusion
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let h = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10
// RwLock<T> — multiple readers OR one writer
let lock = Arc::new(RwLock::new(vec![1, 2, 3]));
{
let r = lock.read().unwrap(); // many readers OK
println!("{:?}", *r);
}
{
let mut w = lock.write().unwrap(); // exclusive write
w.push(4);
}
// Channel (mpsc = multi-producer, single-consumer)
let (tx, rx) = mpsc::channel::<String>();
let tx2 = tx.clone(); // second producer
thread::spawn(move || { tx.send("hello from 1".to_string()).unwrap(); });
thread::spawn(move || { tx2.send("hello from 2".to_string()).unwrap(); });
for received in rx {
println!("{received}");
}
// Rayon — data parallelism (add rayon to Cargo.toml)
use rayon::prelude::*;
let sum: i32 = (0..1_000_000).into_par_iter().sum();
Async / Await
Rust's async is zero-cost — futures do nothing until polled. Use tokio or async-std as the runtime.
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }
use tokio;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = fetch_data("https://api.example.com/data").await?;
println!("{result}");
Ok(())
}
// Async function
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
// tokio::join! — run multiple futures concurrently
async fn run_parallel() {
let (a, b) = tokio::join!(fetch_data("url1"), fetch_data("url2"));
}
// tokio::spawn — independent tasks
async fn run_tasks() {
let h1 = tokio::spawn(async { heavy_work().await });
let h2 = tokio::spawn(async { other_work().await });
let (r1, r2) = tokio::join!(h1, h2);
}
// tokio::select! — first future wins
use tokio::time::{sleep, Duration};
async fn with_timeout() {
tokio::select! {
result = fetch_data("url") => { println!("got data"); }
_ = sleep(Duration::from_secs(5)) => { println!("timeout!"); }
}
}
// Async stream (tokio_stream)
use tokio_stream::StreamExt;
async fn process_stream(mut stream: impl tokio_stream::Stream<Item = i32> + Unpin) {
while let Some(item) = stream.next().await {
println!("{item}");
}
}
// tokio::sync::Mutex for async contexts
use tokio::sync::Mutex as AsyncMutex;
let shared = Arc::new(AsyncMutex::new(0));
let mut guard = shared.lock().await;
*guard += 1;
Smart pointers
// Box<T> — heap allocation
let b = Box::new(5);
println!("{}", *b); // deref
// Recursive types (need Box for known size)
enum List {
Cons(i32, Box<List>),
Nil,
}
// Rc<T> — reference counted (single-threaded)
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a);
println!("{}", Rc::strong_count(&a)); // 2
// RefCell<T> — interior mutability (runtime borrow checking)
use std::cell::RefCell;
let x = RefCell::new(5);
*x.borrow_mut() += 1;
println!("{}", x.borrow()); // 6
// Rc<RefCell<T>> — shared mutable data (single-threaded)
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone = Rc::clone(&shared);
shared.borrow_mut().push(4);
println!("{:?}", clone.borrow()); // [1, 2, 3, 4]
// Arc<T> — atomic reference counting (multi-threaded)
// Arc<Mutex<T>> — shared mutable across threads (see Concurrency section)
// Cow<'a, B> — clone-on-write
use std::borrow::Cow;
fn process(s: &str) -> Cow<str> {
if s.contains("bad") {
Cow::Owned(s.replace("bad", "good"))
} else {
Cow::Borrowed(s)
}
}
Testing
// Unit tests in the same file
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_subtract() {
assert_eq!(subtract(5, 3), 2);
assert_ne!(subtract(5, 3), 0);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_panic() {
panic!("integer overflow");
}
#[test]
fn test_result() -> Result<(), String> {
let n = "42".parse::<i32>().map_err(|e| e.to_string())?;
assert_eq!(n, 42);
Ok(())
}
}
// Integration tests in tests/ directory
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_add_integration() {
assert_eq!(add(10, 20), 30);
}
// Useful assertions
assert_eq!(a, b);
assert_ne!(a, b);
assert!(condition);
assert!(condition, "message {}", value);
// Running tests
// cargo test — all tests
// cargo test test_add — filter by name
// cargo test -- --nocapture — show println output
// cargo test -- --test-threads=1 — sequential
// Async tests (with tokio)
#[cfg(test)]
mod async_tests {
#[tokio::test]
async fn test_fetch() {
let result = fetch_data("url").await;
assert!(result.is_ok());
}
}
Common CLI commands
| Command | Description |
|---|---|
cargo new my-project |
Create new binary project |
cargo new --lib my-lib |
Create new library project |
cargo build |
Build debug binary |
cargo build --release |
Build optimised release binary |
cargo run |
Build and run |
cargo run -- arg1 arg2 |
Run with arguments |
cargo test |
Run all tests |
cargo check |
Fast type-check, no binary |
cargo clippy |
Linting with extra checks |
cargo fmt |
Format all code |
cargo doc --open |
Build and open docs in browser |
cargo add serde |
Add dependency (requires cargo-edit) |
cargo update |
Update Cargo.lock |
cargo publish |
Publish crate to crates.io |
rustup update |
Update Rust toolchain |
rustup target add wasm32-unknown-unknown |
Add WebAssembly target |
Common mistakes
| Mistake | Wrong | Right |
|---|---|---|
| Using moved value | let s2 = s1; println!("{s1}"); |
let s2 = s1.clone(); |
| Unwrap on None/Err | .unwrap() in prod code |
.unwrap_or_else() or ? |
Mixing &str and String |
Wrong return types | Use impl Into<String> or be explicit |
Holding Mutex across .await |
Deadlock | Use tokio::sync::Mutex |
| Mutating while iterating | Compile error | Use retain() or collect then modify |
| Integer overflow | Panic in debug, wrap in release | Use .checked_add() or u32::saturating_add() |
Comparing floats with == |
Silent bugs | Use (a - b).abs() < f64::EPSILON |
Forgetting mut on iterator |
Compile error | let mut iter = v.iter() |
FAQ
What is ownership in Rust?
Every value has exactly one owner. When the owner goes out of scope, the value is dropped (memory freed). Ownership can be moved to another binding or borrowed via references (&T or &mut T). This eliminates use-after-free, double-free, and data races without a garbage collector.
When should I use clone()?
Clone when you need two independent copies of a heap value. It's explicit and visible in code — a sign you're paying the copy cost on purpose. Avoid cloning in hot loops. Often you can avoid cloning by redesigning to pass references instead.
What is the difference between String and &str?
String is a heap-allocated, owned, growable string. &str is a borrowed reference to a string slice — it can point into a String, a string literal, or any UTF-8 data. Function parameters usually take &str for flexibility; return String when you need ownership. Use impl Into<String> to accept both.
When do I use Box, Rc, or Arc?
Use Box<T> for single-owner heap allocation or recursive types. Use Rc<T> for shared ownership within one thread. Use Arc<T> for shared ownership across threads. Pair Rc or Arc with RefCell or Mutex respectively for interior mutability.
What is the difference between iter(), iter_mut(), and into_iter()?
iter() yields &T (shared borrows) — the collection survives. iter_mut() yields &mut T (mutable borrows) — the collection survives but can be modified. into_iter() yields T (owned values) — the collection is consumed.
How do I choose between unwrap(), ?, and match?
Use unwrap() only in tests and throwaway prototypes where a panic is acceptable. Use ? in functions that return Result or Option — it propagates errors cleanly up the call stack. Use match or combinators (map, and_then, unwrap_or) when you need custom error handling logic inline.