Cheatsheet Rust
Linguagem de sistemas com segurança de memória sem garbage collector
Rust
Basic and Types
Variables and Mutability
let x = 5; // immutable let mut y = 10; // mutable y = 20; // OK // Shadowing (new variable): let x = x + 1; // new x let x = x * 2; // another x // Type annotation: let n: i64 = 42; let s: &str = "hello";
In Rust variables are immutable by default — the compiler prevents accidental reassignment. Use mut only when needed. shadowing creates a new variable with the same name (it can even change the type).
Strings
// &str (immutable slice):
let s: &str = "Hello World";
// String (heap, mutable):
let mut s = String::from("Hello");
s.push_str(" World");
s.push('!');
// Formatting:
let msg = format!("Hello, {}!", name);
let n = format!("{:.2}", 3.14159); // "3.14"
let p = format!("{:05}", 42); // "00042"
// Operations:
s.len(); s.contains("World");
s.replace("World", "Rust");Rust distinguishes &str (immutable slice) from String (mutable buffer on the heap). Use &str in parameters to accept both without allocating. The format! macro creates formatted strings without mutation.
Type Aliases
// Alias for long types:
type Result<T> = std::result::Result<T, AppError>;
type Point = (f64, f64);
type Callback = Box<dyn Fn(i32) -> i32>;
// Usage:
fn origin() -> Point {
(0.0, 0.0)
}
// Does not create a new type (just a shortcut):
type Meters = f64;
type Seconds = f64;
// Meters and Seconds are interchangeabletype creates an alias (shortcut) for an existing type — it does not create a new type, so the types are interchangeable. It is useful to simplify long signatures like Result<T, AppError> or complex callbacks.
Constants and Statics
// const: evaluated at compile time const MAX: u32 = 100_000; const PI: f64 = 3.14159; // static: lives for the whole program static NAME: &str = "Rust"; static mut COUNTER: u32 = 0; // Difference: // const is copied on each use // static has a fixed address in memory
const is evaluated at compile time and copied on each use. static lives for the entire program and has a fixed address. Use static mut with care (it requires unsafe).
Type Conversions
// parse (string -> type):
let n: i32 = "42".parse().unwrap();
// the (numeric casting):
let y: u8 = 100i32 the u8;
let z: f64 = 42 the f64;
// to_string:
let s = 42.to_string();
// From / Into (infallible):
let s = String::from("hello");
// TryFrom (may fail):
use std::convert::TryFrom;
let r = u8::try_from(300); // ErrRust does no implicit conversions — all are explicit. parse() converts strings into numbers (returns Result). the casts but may truncate. From/Into are infallible; TryFrom returns Result when it can fail.
Type Inference
// The compiler infers the type: let x = 5; // i32 (default) let f = 3.14; // f64 (default) let v = vec![1, 2]; // Vec<i32> // Inference from usage: let n = "42".parse().unwrap(); // n: i32? i64? — needs context let n: i64 = "42".parse().unwrap(); // Turbofish (::<>): let n = "42".parse::<i32>().unwrap(); let v = vec![1, 2, 3].iter().collect::<Vec<_>>();
The compiler infers types from context, but integers and floats have defaults (i32, f64). When ambiguous, annotate the type or use the turbofish syntax ::<Type> to specify the generic parameter explicitly.
Scalar Types
// Integers: i8, i16, i32, i64, i128, isize u8, u16, u32, u64, u128, usize // Float: let f: f32 = 3.14; let d: f64 = 3.14159265; // Bool and Char (Unicode 4 bytes): let active: bool = true; let emoji: char = '🦀'; // Literals: let dec = 98_222; let hex = 0xff; let bin = 0b1111_0000;
Rust has integers with explicit sizes (i8 to i128, u8 to u128); isize/usize adapt to the architecture. A char is a 4-byte Unicode scalar. Underscores in literals (98_222) improve readability.
Operators
// Arithmetic: + - * / % let r = 10 / 3; // 3 (integer) // Comparison and logical: == != > < >= <= && || ! // Bitwise: & | ^ ! << >> // Range: 1..5 // 1,2,3,4 1..=5 // 1,2,3,4,5 // There is no ++ or -- (use += 1)
Integer division truncates toward zero. The ranges 1..5 (exclusive) and 1..=5 (inclusive) are lazy iterators. Rust has no ++ or -- — use += 1 to avoid precedence ambiguity.
Tuples and Destructuring
let tup = (1, "two", 3.0);
// Access by index:
let a = tup.0; // 1
let b = tup.1; // "two"
// Destructuring:
let (x, y, z) = tup;
// Ignore elements:
let (first, ..) = tup;
let (.., last) = tup;
// Multiple return from a function:
fn divide(a: f64, b: f64) -> (f64, f64) {
(a / b, a % b)
}
let (q, r) = divide(10.0, 3.0);Tuples group values of different types with access by index (.0, .1). Destructuring extracts everything at once; .. ignores the rest. They are ideal for returning multiple values from a function without creating a struct.
Compound Types
// Tuple: let tup: (i32, f64, char) = (500, 6.4, 'A'); let (x, y, z) = tup; // destructuring let first = tup.0; // Array (fixed size, stack): let arr: [i32; 5] = [1, 2, 3, 4, 5]; let zeros = [0; 10]; // 10 zeros // Slice (partial reference): let slice = &arr[1..3]; // [2, 3] // Unit type: let _unit: () = ();
Tuples group different types with access by index (.0) or destructuring. Arrays have a fixed size on the stack. Slices (&arr[1..3]) are views without copying data. The unit type () represents the absence of a value.
Basic Macros
// println! (stdout):
println!("Hello, {}!", name);
println!("{x} + {y} = {}", x + y);
println!("{:#?}", complex_struct);
// eprintln! (stderr):
eprintln!("Error: {}", msg);
// format! (returns String):
let s = format!("{} years", age);
// dbg! (debug):
let x = dbg!(2 + 2);
// assert:
assert!(x > 0);
assert_eq!(a, b);Macros (with !) are expanded at compile time. println! uses {:?} for Debug and {:#?} pretty-printed. dbg! prints the file, line and value. assert!/assert_eq! cause a panic if they fail.
Ownership and Memory
Ownership
// One value = one owner:
let s1 = String::from("hello");
let s2 = s1; // s1 moved! invalid
// println!("{s1}"); // ERROR!
// Clone (deep copy):
let s2 = s1.clone(); // both valid
// Copy types (stack, copied):
let x = 5;
let y = x; // x still valid (i32 is Copy)
// When leaving the scope: automatic dropOwnership is the central concept of Rust — each value has one owner, and when it goes out of scope it is freed (no GC). Assignment moves the value (the original becomes invalid). Simple stack types (i32, bool) are Copy and do not move.
Rc and Arc
// Rc (multiple owners, single-thread):
use std::rc::Rc;
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // ref count = 2
let c = Rc::clone(&a); // ref count = 3
drop(c); // ref count = 2
// Arc (atomic, thread-safe):
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let d2 = Arc::clone(&data);
std::thread::spawn(move || {
println!("{:?}", d2);
});Rc (Reference Counting) allows multiple owners of the same data — it is only freed when the counter reaches zero. It is not thread-safe. Arc (Atomic Rc) is the thread-safe version, with atomic counting — required to share between threads.
Drop and RAII
struct Connection { name: String }
impl Drop for Connection {
fn drop(&mut self) {
println!("Closing {}", self.name);
}
}
{
let l = Connection { name: "DB".into() };
// use the connection...
} // drop() called automatically here
// Force drop earlier:
let l = Connection { name: "DB".into() };
drop(l); // closes now
// l cannot be used afterwardsThe Drop trait runs code when a value goes out of scope (RAII — Resource Acquisition Is Initialization). It is called automatically — ideal for closing files, connections and releasing resources. The drop() function forces early release.
Borrowing and References
// Immutable borrow (&):
fn size(s: &String) -> usize { s.len() }
size(&s1); // s1 still valid
// Mutable borrow (&mut, only 1 at a time):
fn append(s: &mut String) {
s.push_str(" world");
}
append(&mut s1);
// Rules:
// 1. One &mut OR several & (never both)
// 2. References must always be validBorrowing (&) lets you reference without taking ownership. The rules: you can have one &mut OR several &, never both at the same time. This prevents data races and use-after-free at compile time — memory safety with no runtime cost.
RefCell
use std::cell::RefCell;
// Interior mutability:
let data = RefCell::new(vec![1, 2, 3]);
// Mutate through an immutable reference:
data.borrow_mut().push(4);
// Read:
let r = data.borrow();
println!("{:?}", *r);
// Rules checked at RUNTIME:
// - several borrow() OR one borrow_mut()
// - violating causes panic (not a compile error)RefCell allows mutation through an immutable reference (interior mutability). The borrowing rules are checked at runtime (borrow()/borrow_mut()) — violating them causes a panic. Use it when you need to mutate data behind &self.
Stack vs Heap
// Stack (fast, fixed size):
let x = 42; // i32 on the stack
let p = (1.0, 2.0); // tuple on the stack
let a = [0; 100]; // array on the stack
// Heap (Box, Vec, String):
let b = Box::new(42); // i32 on the heap
let v = vec![1, 2, 3]; // data on the heap
let s = String::from("hello");
// Size must be known (Sized):
// fn f(x: dyn Summary) {} // ERROR
fn f(x: &dyn Summary) {} // OK (pointer)The stack is fast but requires a fixed size known at compile time (Sized). Variable-size data (Vec, String) keeps the data on the heap and a pointer on the stack. Trait objects need & or Box because their size is dynamic.
Move and Clone
// Move (heap types):
let v1 = vec![1, 2, 3];
let v2 = v1; // v1 invalid
// Clone (deep copy):
let v2 = v1.clone(); // both valid
// Functions also move:
fn consume(v: Vec<i32>) { }
consume(v2); // v2 moved, invalid
// Borrow to avoid moving:
fn read(v: &Vec<i32>) { }
read(&v1); // v1 remains validTypes with heap data (Vec, String) are moved by default when assigning or passing to functions. clone() makes a deep copy (has a cost). To avoid moving, pass by reference (&) — borrowing.
Cow
use std::borrow::Cow;
fn normalize(s: &str) -> Cow<str> {
if s.contains(' ') {
// Needs modification -> allocates
Cow::Owned(s.replace(' ', "_"))
} else {
// No modification -> borrows
Cow::Borrowed(s)
}
}
let a = normalize("no_space"); // Borrowed
let b = normalize("with space"); // OwnedCow (Clone-On-Write) avoids unnecessary clones: if there is no modification, it borrows (Borrowed); if it needs to change, it allocates (Owned). It is ideal for functions that only sometimes need to transform the input, saving allocations.
Box (heap)
// Allocates on the heap:
let b = Box::new(5);
// Recursive types:
enum List {
Empty,
Node(i32, Box<List>),
}
let list = List::Node(1, Box::new(List::Node(2, Box::new(List::Empty))));
// Trait objects:
let item: Box<dyn Summary> = Box::new(article);
// Big struct on the stack -> heap:
let big = Box::new(HugeStruct::new());Box<T> allocates data on the heap (the stack only holds the pointer). It is required for recursive types (otherwise the size would be infinite), trait objects (Box<dyn Trait>) and to move big structs without copying.
Interior Mutability
struct Cache {
value: RefCell<Option<String>>,
}
impl Cache {
// &self (not &mut) but mutates internally:
fn get_value(&self) -> String {
let mut v = self.value.borrow_mut();
if v.is_none() {
*v = Some(self.compute());
}
v.clone().unwrap()
}
}Interior mutability allows mutating data through &self (an immutable reference), using RefCell, Cell or Mutex inside. It is useful for caches, counters and lazy initialization where the method is logically immutable.
Functions and Closures
Defining Functions
fn add(a: i32, b: i32) -> i32 {
a + b // no ; = return value
}
// No return value (unit):
fn greet(name: &str) {
println!("Hello, {name}!");
}
// Early return:
fn abs(x: i32) -> i32 {
if x < 0 { return -x; }
x
}
fn main() {
println!("{}", add(2, 3));
}Functions declare types for all parameters (mandatory). The implicit return is the last expression without ; — return is for early exits. Without ->, the function returns () (unit).
Generic Functions
// Trait bound:
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Where clause:
fn process<T, U>(t: T, u: U) -> String
where
T: Display + Clone,
U: Debug,
{
format!("{t}: {u:?}")
}
// impl Trait in the return:
fn make_iter() -> impl Iterator<Item = i32> {
(0..10).filter(|x| x % 2 == 0)
}Generics work with any type that satisfies the trait bounds — with no runtime cost (monomorphization). The where clause organizes complex bounds. impl Trait in the return hides the concrete type (useful for iterators and closures).
Const Functions
// const fn: evaluable at compile time:
const fn square(x: u32) -> u32 {
x * x
}
const AREA: u32 = square(10); // 100
// Can also be used at runtime:
let n = square(5); // 25
// Limitations (fewer ops allowed):
const fn max(a: u32, b: u32) -> u32 {
if a > b { a } else { b }
}A const fn can be evaluated at compile time when the arguments are constants. It also works normally at runtime. It has limitations (allowed operations), but lets you compute values for const and arrays.
Closures
// Syntax:
let add = |a, b| a + b;
let twice = |x: i32| -> i32 { x * 2 };
// Captures the environment:
let factor = 3;
let triple = |x| x * factor;
// As an argument:
let mut nums = vec![3, 1, 4, 1, 5];
nums.sort_by(|a, b| a.cmp(b));
nums.iter().filter(|&&x| x > 2);Closures are anonymous functions that capture variables from the environment. The compiler infers the types (annotations optional). The |params| body syntax is compact and ideal for callbacks in iterators and sorting.
Methods (impl)
struct Circle { radius: f64 }
impl Circle {
// Constructor (associated method):
fn new(radius: f64) -> Self {
Circle { radius }
}
// Borrow (&self):
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius.powi(2)
}
// Mutable (&mut self):
fn scale(&mut self, f: f64) {
self.radius *= f;
}
}impl blocks attach methods to structs. The first parameter defines the receiver: &self (borrow), &mut self (mutation) or self (consume). Functions without self are associated methods called with Circle::new().
Parameters and Returns
// Multiple returns (tuple):
fn divide(a: f64, b: f64) -> (f64, f64) {
(a / b, a % b)
}
let (q, r) = divide(10.0, 3.0);
// Named return (does not exist):
// Rust has no named returns
// never type (!):
fn fail() -> ! {
panic!("fatal error");
}
// impl Trait the a parameter:
fn show(item: impl Display) {
println!("{item}");
}For multiple return values use tuples with destructuring. The never type ! marks functions that never return (like panic!). impl Trait the a parameter is sugar for a generic with a trait bound.
Closures with move
let data = vec![1, 2, 3];
// move takes ownership:
let closure = move || {
println!("{:?}", data);
};
closure();
// Required for threads:
let handle = std::thread::spawn(move || {
// data was moved into the thread
data.len()
});
// Without move: the closure only borrows
// (may cause an error if data goes out of scope)move forces the closure to take ownership of the captured values. It is essential when the closure outlives the scope where it was created — for example, when sending it to another thread with thread::spawn.
Function Pointers
fn double(x: i32) -> i32 { x * 2 }
// fn is a type (does not capture environment):
let f: fn(i32) -> i32 = double;
println!("{}", f(5)); // 10
// As a parameter:
fn apply(v: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
v.iter().map(|&x| f(x)).collect()
}
// Return a closure:
fn multiplier(f: i32) -> impl Fn(i32) -> i32 {
move |x| x * f
}The fn type (lowercase) is a function pointer — unlike closures, it does not capture the environment. It is useful for passing named functions without generics overhead. To return closures use impl Fn (hides the anonymous type).
Function Traits
// Fn (borrow, called many times):
fn apply(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
// FnMut (mutates the environment):
fn counter(f: &mut dyn FnMut()) {
f(); f();
}
// FnOnce (consumed, called once):
fn run(f: impl FnOnce() -> String) {
println!("{}", f());
}
// Usage:
let r = apply(&|x| x * 2, 5); // 10Rust has three function traits: Fn (read-only, called many times), FnMut (may mutate captured variables) and FnOnce (consumed when called). The compiler picks which one the closure implements automatically.
Recursion
// Factorial:
fn factorial(n: u64) -> u64 {
if n <= 1 { 1 } else { n * factorial(n - 1) }
}
// Fibonacci:
fn fib(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fib(n - 1) + fib(n - 2),
}
}
// Tail recursion:
fn sum(n: u64, acc: u64) -> u64 {
if n == 0 { acc } else { sum(n - 1, acc + n) }
}Rust supports recursion, but the compiler does not guarantee tail-call optimization. For deep recursion prefer iteration or tail recursion with an accumulator. match is ideal for defining base and recursive cases clearly.
Collections
Vec (vector)
// Create:
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
let zeros = vec![0; 10];
// Modify:
v.push(6);
v.pop(); // Option<i32>
v.insert(0, 0);
v.retain(|&x| x > 2);
// Access:
v[0]; // panics if out of bounds
v.get(0); // Option<&i32>
v.first(); v.last();
v.len(); v.is_empty();
// Iterate:
for x in &v { println!("{x}"); }Vec<T> is the most used collection — a dynamic array on the heap. v[i] panics if the index is invalid; v.get(i) returns Option (safe). retain() filters in-place without allocating a new vector.
String Operations
let mut s = String::from("Hello World");
// Append:
s.push('!');
s.push_str(" Rust");
// Transform:
s.to_uppercase();
s.replace("World", "Rust");
s.trim().to_string();
// Split:
let parts: Vec<&str> = s.split(' ').collect();
let lines: Vec<&str> = text.lines().collect();
// Check:
s.contains("World");
s.starts_with("Hello");
s.len(); // bytes!
s.chars().count(); // charactersString in Rust is UTF-8 — len() returns bytes, not characters (an emoji can be 4 bytes). To count characters use chars().count(). split() returns a zero-copy iterator of &str. lines() splits by lines.
VecDeque
use std::collections::VecDeque; let mut queue = VecDeque::new(); // Insert at both ends: queue.push_back(1); // [1] queue.push_back(2); // [1, 2] queue.push_front(0); // [0, 1, 2] // Remove from both ends: queue.pop_front(); // Some(0) queue.pop_back(); // Some(2) // Access: queue.front(); queue.back(); queue.get(0); queue.len();
VecDeque is a double-ended queue (deque) — O(1) insertion and removal at both ends. Use it when you need a queue (FIFO) or a stack with access to both ends. A Vec is only efficient inserting/removing at the end.
HashMap
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Anna", 95);
scores.insert("Ray", 87);
// Access:
let v = scores.get("Anna"); // Option<&i32>
// Entry API:
scores.entry("Eve").or_insert(100);
scores.entry("Anna").and_modify(|v| *v += 5);
// Iterate:
for (k, v) in &scores {
println!("{k}: {v}");
}
scores.contains_key("Anna");
scores.remove("Ray");HashMap offers O(1) lookup by key. The entry() API is idiomatic: or_insert() inserts if absent, and_modify() updates if present — avoiding a double lookup. get() returns Option; iteration order is not deterministic.
HashSet and BTreeMap
use std::collections::{HashSet, BTreeMap};
// HashSet (unique, unordered):
let mut set = HashSet::new();
set.insert(1); set.insert(2); set.insert(2);
set.len(); // 2
set.contains(&1); // true
// Set operations:
let union_set: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();
// BTreeMap (sorted by key):
let mut bt = BTreeMap::new();
bt.insert("banana", 2);
bt.insert("apple", 1);
// Iterates in order: apple, bananaHashSet guarantees uniqueness with O(1) lookup and supports set operations (union, intersection, difference). BTreeMap/BTreeSet keep elements sorted (O(log n)). Choose HashMap for performance, BTree when order matters.
BinaryHeap
use std::collections::BinaryHeap; let mut heap = BinaryHeap::new(); heap.push(3); heap.push(1); heap.push(4); // Max-heap (largest first): heap.peek(); // Some(&4) heap.pop(); // Some(4) heap.pop(); // Some(3) // Min-heap with Reverse: use std::cmp::Reverse; let mut min = BinaryHeap::new(); min.push(Reverse(3)); min.push(Reverse(1)); min.pop(); // Some(Reverse(1))
BinaryHeap is a priority queue (max-heap by default — the largest comes out first). peek() looks at the top without removing, pop() removes it. For a min-heap, wrap the values in Reverse. Useful for top-K and scheduling.
Iterators — Adapters
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
// map / filter:
let doubles: Vec<_> = nums.iter().map(|&x| x * 2).collect();
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// filter_map (Option):
let valid: Vec<_> = data.iter()
.filter_map(|s| s.parse::<i32>().ok()).collect();
// flat_map / zip / chain:
let flat: Vec<_> = words.iter().flat_map(|s| s.chars()).collect();
let pairs: Vec<_> = a.iter().zip(b.iter()).collect();
let all: Vec<_> = v1.iter().chain(v2.iter()).collect();
// take / skip:
nums.iter().take(3);
nums.iter().skip(2);Adapters are lazy — they do not run until consumed. map transforms, filter selects, filter_map combines both for Option. flat_map flattens results, zip pairs and chain concatenates. Zero-cost.
Slices and Windows
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
// Slices:
let slice = &data[2..5]; // [3, 4, 5]
// chunks:
for chunk in data.chunks(3) {
println!("{:?}", chunk); // [1,2,3] [4,5,6] [7,8]
}
// windows (sliding):
for w in data.windows(3) {
println!("{:?}", w); // [1,2,3] [2,3,4] ...
}
// split by predicate:
let v = vec![1, 0, 2, 0, 3];
for part in v.split(|&x| x == 0) {
println!("{:?}", part);
}Slices (&vec[2..5]) are zero-copy views. chunks() splits into fixed-size blocks (the last one may be smaller); windows() creates overlapping sliding windows (useful for moving averages). split() splits by a predicate.
Custom Iterators
struct Fibonacci { a: u64, b: u64 }
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let next_val = self.a + self.b;
self.a = self.b;
self.b = next_val;
Some(self.a)
}
}
let fibs = Fibonacci { a: 0, b: 1 };
let first_ten: Vec<_> = fibs.take(10).collect();Implement the Iterator trait (with type Item and next()) to create custom iterators. You get all the adapters (map, filter, take...) for free. Return None to finish (or use infinite iterators).
Iterators — Consumers
let nums = vec![5, 2, 8, 1, 9, 3];
// collect / fold:
let v: Vec<_> = nums.iter().collect();
let total = nums.iter().fold(0, |acc, &x| acc + x);
let total2: i32 = nums.iter().sum();
// Search:
nums.iter().find(|&&x| x > 5); // Option
nums.iter().position(|&x| x == 8); // Option<usize>
nums.iter().any(|&x| x > 8); // bool
nums.iter().all(|&x| x > 0); // bool
// Extremes and partition:
nums.iter().min(); nums.iter().max();
let (evens, odds): (Vec<_>, Vec<_>) =
nums.iter().partition(|&&x| x % 2 == 0);Consumers end the chain and produce a result. collect() converts to Vec, HashMap, String, etc. fold is the general reduce. find/position return Option. partition splits into two groups in a single pass.
Sorting
let mut v = vec![3, 1, 4, 1, 5, 9]; // Natural order: v.sort(); // [1,1,3,4,5,9] // Descending: v.sort_by(|a, b| b.cmp(a)); // By key: let mut people = vec![ana, rui, eva]; people.sort_by_key(|p| p.age); // Faster (not stable): v.sort_unstable(); // Remove consecutive duplicates: v.sort(); v.dedup();
sort() is stable (preserves the order of equals); sort_unstable() is faster but does not. sort_by_key() sorts by an extracted key. dedup() removes consecutive duplicates — sort first to remove them all.
Errors and Result
Result and ?
use std::num::ParseIntError;
fn read_number(s: &str) -> Result<i32, ParseIntError> {
let n = s.trim().parse::<i32>()?;
Ok(n * 2)
}
// ? propagates the error (early return Err)
fn pipeline(s: &str) -> Result<i32, ParseIntError> {
let a = read_number(s)?;
let b = read_number("10")?;
Ok(a + b)
}Rust has no exceptions — it uses Result<T, E> for recoverable errors. The ? operator propagates the error automatically (early return with Err) or extracts the value on Ok. It keeps error handling code clean and explicit.
unwrap_or and Combinators
// Defaults:
let n = "abc".parse::<i32>().unwrap_or(0);
let n = "abc".parse().unwrap_or_else(|_| 0);
// Chain operations:
let result = "42".parse::<i32>()
.map(|n| n * 2)
.map(|n| n + 1)
.unwrap_or(0); // 85
// and_then (operations returning Option):
let n = Some("42")
.and_then(|s| s.parse::<i32>().ok())
.map(|n| n * 2);Combinators chain operations without a verbose match. map transforms the value, and_then chains operations that also return Option/Result. unwrap_or_else computes the default lazily only when needed.
Error Recovery
use std::fs::{self, File};
use std::io::ErrorKind;
// match on the error kind:
let f = match File::open("data.txt") {
Ok(f) => f,
Err(e) if e.kind() == ErrorKind::NotFound => {
File::create("data.txt").unwrap()
}
Err(e) => panic!("error: {e}"),
};
// or_else (lazy):
let f = File::open("data.txt")
.or_else(|e| File::create("data.txt"));To recover from errors, use match inspecting the ErrorKind (e.g. NotFound) and take an alternative action (like creating the file). The or_else offers a lazy alternative on Err, keeping the functional chaining.
Option
// Option: the value may not exist
fn find_index(v: &[i32], target: i32) -> Option<usize> {
v.iter().position(|&x| x == target)
}
match find_index(&data, 5) {
Some(i) => println!("Index {i}"),
None => println!("Not found"),
}
// Useful methods:
value.unwrap_or(0);
value.unwrap_or_else(|| compute());
value.map(|x| x * 2);
value.is_some(); value.is_none();Option<T> (Some/None) replaces null — the compiler forces you to handle absence. unwrap_or gives a default, map transforms the present value. Methods like is_some()/is_none() check the state.
anyhow
// anyhow = dynamic errors (easy)
use anyhow::{Result, Context};
fn read_config() -> Result<String> {
let s = std::fs::read_to_string("config.toml")
.context("failed to read config")?;
Ok(s)
}
fn main() -> Result<()> {
let cfg = read_config()?;
println!("{cfg}");
Ok(())
}The anyhow crate provides a dynamic error type (anyhow::Result) ideal for applications — it accepts any error via ?. The .context() adds messages to the error. Use it in binaries; in libraries prefer typed errors.
Custom Errors
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
Io(std::io::Error),
}
// Automatic conversion (for ?):
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e)
}
}
fn read_file() -> Result<String, AppError> {
let s = std::fs::read_to_string("f.txt")?;
Ok(s)
}Define errors the enums with a variant for each case. Implement From<E> for automatic conversion — that way ? converts library errors into your type. Implement Display and Error for ecosystem integration.
thiserror
// thiserror = typed errors (libraries)
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("IO error")]
Io(#[from] std::io::Error),
}
fn fetch(id: u32) -> Result<(), AppError> {
Err(AppError::NotFound(format!("item {id}")))
}The thiserror crate generates the Error and Display implementations automatically via derive. The #[from] creates the From conversion for ?. It is the default choice for libraries that expose typed errors to consumers.
panic and unwrap
// panic! (unrecoverable error):
panic!("impossible state");
// unwrap (panics if None/Err):
let n = "42".parse::<i32>().unwrap();
let v = Some(5).unwrap();
// expect (panic with message):
let n = "42".parse::<i32>()
.expect("should be a number");
// Out-of-bounds index = panic:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
let x = v.get(10); // None (safe)panic! is for unrecoverable errors (it aborts the thread). unwrap() panics on None/Err — use it only in prototypes or when you are certain. expect() is the same but with a descriptive message. Prefer get() over direct indexing.
main with Result
use std::fs;
// main can return Result:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string("data.txt")?;
println!("{content}");
Ok(())
}
// On error, prints it and exits with code 1
// Equivalent to try! on every lineThe main function can return Result — on Err, the program prints the error and exits with code 1. This allows using ? at the top of the program without a manual match. Use Box<dyn Error> to accept any error.
Flow Control
If / Else
// if is an expression (returns a value):
let x = if condition { 10 } else { 20 };
// if/else if/else chain:
let msg = if grade >= 18 {
"Excellent"
} else if grade >= 10 {
"Passed"
} else {
"Failed"
};
// No parentheses around the condition:
if x > 0 {
println!("Positive");
}In Rust if is an expression that returns a value — it can be assigned directly, removing the need for a ternary operator. It uses no parentheses around the condition. All branches must return the same type.
Loops
// loop (infinite, returns a value):
let r = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
// while:
let mut n = 5;
while n > 0 {
n -= 1;
}
// for (iterator):
for i in 0..5 {
println!("{i}");
}
// enumerate:
for (i, v) in items.iter().enumerate() {
println!("{i}: {v}");
}Rust has three loops: loop (infinite, can return a value via break), while (condition) and for (consumes iterators — the most common). The for never has off-by-one errors. enumerate() gives index and value.
Ranges
// Exclusive range:
for i in 0..5 { } // 0,1,2,3,4
// Inclusive range:
for i in 0..=5 { } // 0,1,2,3,4,5
// In slices:
let v = vec![1, 2, 3, 4, 5];
let middle = &v[1..4]; // [2,3,4]
let tail = &v[3..]; // [4,5]
// As an iterator:
let total: i32 = (1..=100).sum(); // 5050
let evens: Vec<_> = (0..10)
.step_by(2).collect(); // [0,2,4,6,8]The ranges 0..5 (exclusive) and 0..=5 (inclusive) are lazy iterators. They are used in for, slicing and methods like sum(). step_by() sets the iteration step.
Match
// Must be exhaustive:
let msg = match number {
1 => "one",
2 => "two",
3..=5 => "three to five",
_ => "other", // wildcard
};
// Destructuring:
match point {
(0, 0) => println!("Origin"),
(x, 0) => println!("X axis: {x}"),
(x, y) => println!("({x}, {y})"),
}
// match returns a valuematch is the pattern matching of Rust — more powerful than switch. The compiler requires exhaustiveness (all cases or _), preventing forgotten cases. It destructures tuples, structs and enums automatically.
Labels and Control
// Loop labels:
'outer: for i in 0..5 {
for j in 0..5 {
if i * j > 12 {
break 'outer;
}
if j % 2 == 0 {
continue 'outer;
}
}
}
// break with value in a nested loop:
let r = 'calc: loop {
for x in data.iter() {
if *x > 100 { break 'calc *x; }
}
break 0;
};Labels ('outer:) identify loops for break/continue in nested loops — without labels, only the innermost loop is affected. Combined with break with a value, it returns results from nested loops without boolean flags.
Result in match
// Result: Ok(value) or Err(error)
match "42".parse::<i32>() {
Ok(n) => println!("Number: {n}"),
Err(e) => println!("Error: {e}"),
}
// if let with Result:
if let Ok(n) = input.parse::<i32>() {
println!("Valid: {n}");
}
// unwrap_or (default):
let n = "abc".parse::<i32>().unwrap_or(0);
// map (transforms Ok):
let doubled = "21".parse::<i32>()
.map(|n| n * 2); // Ok(42)Result (Ok/Err) replaces exceptions. if let Ok handles only the success case. unwrap_or gives a default value on error. map transforms the value inside Ok without touching the error.
Match Guards
// Guard with if:
match x {
n if n < 0 => println!("Negative"),
0 => println!("Zero"),
n => println!("Positive: {n}"),
}
// Guard with patterns:
match point {
(x, y) if x == y => println!("Diagonal"),
(x, y) => println!("({x}, {y})"),
}
// Bindings:
match value {
n @ 1..=10 => println!("Small: {n}"),
n => println!("Large: {n}"),
}match guards (an if after the pattern) add arbitrary conditions to each arm. The binding n @ pattern names the variable while testing the pattern — useful for named ranges.
Destructuring Patterns
// Structs:
let Point { x, y } = point;
let Point { x: px, y: py } = point;
let Point { x, .. } = point; // ignores y
// Enums:
match msg {
Message::Move { x, y } => { }
Message::Write(s) => { }
Message::Color(r, g, b) => { }
}
// References and nested:
let &(a, b) = &(1, 2);
match value {
Some(Some(x)) => println!("{x}"),
_ => {},
}Destructuring extracts values from structs and enums directly. The .. pattern ignores unneeded fields. You can rename with x: px. Nested patterns (Some(Some(x))) unwrap several layers of Option in one line.
Option and if let
// Option: Some(value) or None
match value {
Some(v) => println!("Has: {v}"),
None => println!("Empty"),
}
// if let (a single pattern):
if let Some(v) = value {
println!("Has: {v}");
}
// while let:
while let Some(item) = stack.pop() {
println!("{item}");
}
// let-else (Rust 1.65+):
let Some(v) = value else {
return;
};Option (Some/None) replaces null. if let is sugar for a match with a single pattern. while let iterates until the pattern fails. let-else extracts the value or does an early return.
Expressions and Blocks
// Blocks are expressions:
let x = {
let a = 3;
let b = 4;
a + b // no ; = return value
};
// if/match/loop the an expression:
let max = if a > b { a } else { b };
let abs = match x {
n if n < 0 => -n,
n => n,
};
let val = loop { break 42; };Rust is expression-based: almost everything returns a value. The last line of a block without ; is the returned value. This removes temporary mutable variables — if/match/loop are used the expressions. The ; discards the value.
Structs, Enums e Traits
Structs
// Named fields:
struct User {
name: String,
age: u32,
active: bool,
}
let user = User {
name: String::from("Anna"),
age: 30,
active: true,
};
// Field init shorthand:
fn create(name: String, age: u32) -> User {
User { name, age, active: true }
}
// Struct update:
let u2 = User { name: "Ray".into(), ..user };Structs are the main data composition mechanism (there are no classes). The field init shorthand avoids repetition when the variable has the same name the the field. The spread ..user copies the unspecified fields from another instance.
Traits
// Define trait:
trait Summary {
fn summarize(&self) -> String;
// Default method:
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20])
}
}
// Implement:
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
// Trait bound:
fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}Traits define shared behavior (like interfaces, but more powerful). They can have default methods with an implementation. Use impl Trait in parameters for static dispatch (zero cost) or dyn Trait for dynamic dispatch.
Operator Overloading
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Point(f64, f64);
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point(self.0 + other.0, self.1 + other.1)
}
}
let p = Point(1.0, 2.0) + Point(3.0, 4.0);
// Point(4.0, 6.0)Operator overloading is done by implementing traits from std::ops (Add, Sub, Mul...). The type Output defines the result type. It allows using +, -, * with custom types naturally.
Tuple and Unit Structs
// Tuple struct:
struct Point(f64, f64);
let p = Point(3.0, 4.0);
let x = p.0;
// Unit struct:
struct Marker;
// Newtype pattern:
struct Meters(f64);
struct Seconds(f64);
fn speed(d: Meters, t: Seconds) -> f64 {
d.0 / t.0
}Tuple structs create distinct types with the same structure — Point(f64,f64) is different from Color(f64,f64). Unit structs hold no data (useful the markers). The newtype pattern prevents mixing up types with the same representation.
Trait Objects (dyn)
// Heterogeneous collection:
let items: Vec<Box<dyn Summary>> = vec![
Box::new(article),
Box::new(tweet),
];
for item in &items {
println!("{}", item.summarize());
}
// Dynamic parameter:
fn show_all(list: &[Box<dyn Summary>]) {
for item in list {
println!("{}", item.preview());
}
}Trait objects (dyn Trait) enable dynamic polymorphism via vtable — useful for heterogeneous collections (Vec<Box<dyn Trait>>). They have a small runtime cost; prefer generics (impl Trait) when possible.
Enums the State Machines
enum State {
Draft { content: String },
Review { content: String },
Published { content: String },
}
impl State {
fn submit(self) -> Result<Self, String> {
match self {
State::Draft { content } =>
Ok(State::Review { content }),
_ => Err("Drafts only".into()),
}
}
}The typestate pattern uses enums to model state machines where invalid transitions are impossible at compile time. The methods consume self (ownership) and return a new state — the old one ceases to exist.
Enums
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
Color(u8, u8, u8),
}
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("({x},{y})"),
Message::Write(s) => println!("{s}"),
Message::Color(r, g, b) => println!("#{r:02x}{g:02x}{b:02x}"),
}
}
}Enums in Rust are sum types (tagged unions) — each variant can hold different data. Combined with match, they guarantee exhaustiveness. Option and Result are generic enums that make invalid states unrepresentable.
Derive
// Common derive:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Config {
host: String,
port: u16,
}
// Derivable traits:
// Debug, Clone, Copy, PartialEq, Eq
// PartialOrd, Ord, Hash, Default
// Usage:
let c = Config { host: "localhost".into(), port: 8080 };
println!("{:?}", c); // Debug
let c2 = c.clone(); // Clone
assert_eq!(c, c2); // PartialEq#[derive] generates automatic implementations. Debug enables {:?}, Clone enables .clone(), PartialEq/Eq enable ==. Copy is for simple types copied bit-by-bit. Display is not derivable.
Lifetimes
// Lifetime in references:
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
// In structs:
struct Excerpt<'a> {
text: &'a str,
}
// 'static:
let s: &'static str = "lives forever";
// Elision (no annotation needed):
fn first(s: &str) -> &str { &s[..1] }Lifetimes ('a) guarantee that references do not outlive the data they point to — preventing dangling pointers without a GC. The elision rules infer lifetimes in most cases. Only annotate when there is ambiguity. 'static lives for the whole program.
Generics in Structs
struct Point<T> {
x: T,
y: T,
}
impl<T: std::fmt::Display> Point<T> {
fn show(&self) {
println!("({}, {})", self.x, self.y);
}
}
// Only for f64:
impl Point<f64> {
fn distance(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}Generic structs work with any type — the compiler generates specialized code (monomorphization, zero cost). You can constrain with trait bounds on the impl and have specific impls for concrete types (Point<f64>).
Associated Types
trait MyIterator {
type Item;
fn next_item(&mut self) -> Option<Self::Item>;
}
struct Counter { current: u32 }
impl MyIterator for Counter {
type Item = u32;
fn next_item(&mut self) -> Option<u32> {
self.current += 1;
Some(self.current)
}
}Associated types (type Item) define dependent types inside the trait — more ergonomic than extra generics. Each implementation fixes the concrete type. It is the pattern used by the std Iterator trait (Item).
Concurrency and Async
Threads
use std::thread;
// Spawn:
let handle = thread::spawn(|| {
println!("In another thread!");
});
handle.join().unwrap(); // waits
// move (takes ownership):
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
println!("{:?}", data);
});
// Multiple threads:
let handles: Vec<_> = (0..5)
.map(|i| thread::spawn(move || i * 2))
.collect();thread::spawn creates an OS thread and returns a JoinHandle. The join() waits for the thread to finish and returns its result. Use move to transfer ownership of the data into the thread. The compiler rejects code with potential data races.
Atomics
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
let counter = Arc::new(AtomicUsize::new(0));
for _ in 0..10 {
let c = Arc::clone(&counter);
thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
});
}
// Without Mutex (lighter):
println!("{}", counter.load(Ordering::SeqCst));Atomic types (AtomicUsize, AtomicBool...) allow thread-safe operations without a Mutex — lighter for counters and flags. The fetch_add increments atomically. The Ordering controls memory consistency.
Thread Pool (rayon)
// rayon = easy data parallelism
use rayon::prelude::*;
let v: Vec<i32> = (0..1_000_000).collect();
// par_iter (automatic parallelism):
let total: i64 = v.par_iter()
.map(|&x| x the i64 * x the i64)
.sum();
// Parallel sorting:
let mut data = vec![3, 1, 4, 1, 5];
data.par_sort();
// Parallel filter:
let evens: Vec<_> = v.par_iter()
.filter(|&&x| x % 2 == 0)
.collect();The rayon crate offers data parallelism with almost zero effort — swap iter() for par_iter() and the work is split automatically across CPU colors. Ideal for CPU-bound work (map, filter, sort on large collections).
Channels (mpsc)
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("hello").unwrap();
});
let msg = rx.recv().unwrap(); // blocks
println!("{msg}");
// Multiple producers:
let tx2 = tx.clone();
// Iterate messages:
for msg in rx {
println!("{msg}");
}Channels follow the principle "share memory by communicating". The mpsc (multiple producer, single consumer) has tx (sending) and rx (receiving). The send() moves the value (ownership). The recv() blocks until a message arrives; iterating over rx ends when all the tx are dropped.
Async/Await
// async fn returns a Future (lazy):
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
// .await runs the future:
async fn process() {
let data = fetch_data("https://api.example.com")
.await.unwrap();
println!("{data}");
}async fn returns a Future (lazy — it does not run until .await). The syntax looks synchronous but the execution is non-blocking — ideal for I/O intensive work (HTTP, DB). It needs a runtime (like tokio) to execute the futures.
Scoped Threads
use std::thread;
let data = vec![1, 2, 3, 4, 5];
let mut results = vec![];
// scope allows borrowing without Arc:
thread::scope(|s| {
for chunk in data.chunks(2) {
s.spawn(|| {
// borrows chunk and results
let total: i32 = chunk.iter().sum();
println!("sum: {total}");
});
}
}); // all threads finish herethread::scope creates threads that can borrow (&) data from the enclosing scope without Arc or move — the compiler guarantees they all finish before the data leaves the scope. Simpler and safer for parallelism with local data.
Arc and Mutex
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut val = c.lock().unwrap();
*val += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10Arc (Atomic Rc) shares data across threads; Mutex guarantees exclusive access (one thread at a time via lock()). Together they enable safe shared mutable state. The lock() returns a guard that releases the mutex when leaving the scope.
Tokio Runtime
// Cargo.toml:
// tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
let result = process().await;
}
// Concurrency with join!:
let (a, b) = tokio::join!(
fetch_data("url1"),
fetch_data("url2")
);
// Spawn tasks:
let handle = tokio::spawn(async {
heavy_work().await
});
let r = handle.await.unwrap();tokio is the standard async runtime — it manages thousands of concurrent tasks on a pool of OS threads. The join! runs futures in parallel and waits for all. The tokio::spawn creates independent tasks (lighter than threads). The #[tokio::main] attribute sets up the runtime.
Send and Sync
// Send: can be moved between threads // Sync: can be shared (&T) between threads // Most types are Send + Sync: // i32, String, Vec<T>, Arc<T> // Rc is NOT Send nor Sync: // let r = Rc::new(5); // thread::spawn(move || r); // ERROR // Mutex<T> is Sync, RefCell is NOT: // (Mutex checks at runtime, RefCell is not thread-safe) // The compiler checks automatically
Send marks types that can be moved between threads; Sync those that can be shared by reference. Most types are both. Rc is neither (use Arc); RefCell is not Sync (use Mutex). The compiler checks everything automatically.
Concurrent Async
use futures::future::join_all;
// Several tasks in parallel:
async fn fetch_all(urls: Vec<String>) -> Vec<String> {
let futures = urls.iter()
.map(|u| fetch_data(u));
join_all(futures).await
.into_iter()
.filter_map(|r| r.ok())
.collect()
}
// select! (first to finish):
tokio::select! {
r = operation_a() => println!("A: {r}"),
r = operation_b() => println!("B: {r}"),
}The join_all runs several futures concurrently and waits for all of them. The select! waits for the first one to finish (a race) — useful for timeouts and cancellation. This approach scales to thousands of I/O operations without creating an OS thread for each one.
Position and Tools
Cargo
# Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Commands:
cargo new project # create
cargo build # compile
cargo run # compile + run
cargo test # tests
cargo add serde # add dep
cargo build --release # optimizedCargo is the build system and package manager — it compiles, manages dependencies, runs tests and publishes crates. Dependencies live in Cargo.toml. The --release enables optimizations. The cargo add adds dependencies automatically.
Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "error")]
fn test_fail() {
panic!("expected error");
}
#[test]
fn test_result() -> Result<(), String> {
if 2 + 2 == 4 { Ok(()) }
else { Err("math failed".into()) }
}
}Tests live in #[cfg(test)] (compiled only for testing). The #[test] marks test functions. assert_eq!/assert! verify conditions. #[should_panic] tests expected panics. Tests can return Result to use ?.
Features and Profiles
# Cargo.toml - optional features:
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml"]
# Usage in code:
#[cfg(feature = "json")]
fn parse_json() { }
# Build profiles:
[profile.release]
opt-level = 3
lto = true
strip = trueFeatures are optional functionality you can enable (--features full) — they allow conditional dependencies. Profiles control optimization: dev (fast to compile) and release (optimized). opt-level, lto and strip fine-tune the final binary.
Modules
// src/main.rs or src/lib.rs
mod utils {
pub fn helper() { }
pub(crate) fn internal() { }
fn private() { } // only in this module
}
use utils::helper;
// Files:
// src/utils.rs -> mod utils
// src/utils/mod.rs -> alternative
// src/utils/db.rs -> mod utils { mod db }
// Re-export:
pub use utils::helper;Modules organize code and control visibility. Everything is private by default — pub exposes, pub(crate) limits to the crate. Modules map to files: mod utils looks for src/utils.rs. The use imports paths into scope.
Documentation
/// Adds two numbers.
///
/// # Examples
/// ```
/// let r = add(2, 3);
/// assert_eq!(r, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
//! Module/crate documentation
//! (at the top of the file)
// cargo doc --open (generates HTML)Doc comments (///) document items and support Markdown. The ``` blocks are runnable tests (cargo test runs them). //! documents the module/crate. cargo doc --open generates the HTML documentation (like docs.rs).
Essential Crates
# Web: axum / actix-web # web frameworks reqwest # HTTP client serde / serde_json # serialization # Async and data: tokio # async runtime sqlx / diesel # databases rayon # parallelism # Quality: anyhow / thiserror # errors clap # CLI args tracing # logging
The Rust ecosystem (crates.io) has mature libraries: serde for serialization, tokio for async, reqwest for HTTP, axum/actix-web for servers. anyhow/thiserror for errors and clap for CLIs.
Visibility
pub struct Config {
pub host: String, // public
pub(crate) port: u16, // crate only
token: String, // private
}
pub mod api {
pub(super) fn internal() { } // parent module
pub fn external() { }
}
// Enum: variants follow the enum visibility
pub enum State {
Active, // public
Inactive, // public
}Visibility is opt-in: pub (public), pub(crate) (inside the crate), pub(super) (parent module) or private (no modifier). Struct fields are private by default. Enum variants follow the visibility of the enum itself.
Clippy and fmt
# Clippy (official linter): cargo clippy cargo clippy --fix # fixes automatically # Warning example: # if x == true -> if x # vec![].len() == 0 -> is_empty() # rustfmt (formatting): cargo fmt # formats everything cargo fmt --check # check only (CI) # rustfmt.toml (config): # max_width = 100 # tab_spaces = 4
cargo clippy is the official linter with hundreds of rules that catch non-idiomatic code and potential bugs. The cargo fmt formats automatically following the community standard style. Both should run in CI for consistency.
Declarative Macros
// macro_rules! (pattern matching on tokens):
macro_rules! compute {
($a:expr, +, $b:expr) => { $a + $b };
($a:expr, *, $b:expr) => { $a * $b };
}
let r = compute!(3, +, 4); // 7
// Repetition:
macro_rules! vec_strs {
($($x:expr),*) => {
vec![$($x.to_string()),*]
};
}
let v = vec_strs!["a", "b", "c"];macro_rules! pattern-matches on tokens and expands to code at compile time. The $x:expr patterns capture expressions; $($x),* repeats zero or more times. Macros avoid repetition while keeping type-safety (unlike C macros).
Attributes
// Conditional compilation:
#[cfg(target_os = "windows")]
fn path() -> &str { "C:\\" }
#[cfg(feature = "extra")]
fn extra_feature() { }
// Suppress warnings:
#[allow(unused_variables)]
let x = 5;
#![allow(dead_code)] // whole crate
// Derive:
#[derive(Debug, Clone)]
struct Point { x: f64, y: f64 }Attributes (#[...]) give metadata to the compiler. #[cfg] compiles conditionally (per OS, feature...). #[allow] suppresses specific warnings. #[derive] generates trait implementations. #![...] applies to the enclosing item (e.g. the crate).