Rust Syntax Basics
A three-part reference: the language core for solving problems, the algorithmic pattern templates, and the bridge from puzzles to real backend development. All code in here was compiled and verified.
Contents
Part 1 — Syntax & Data Structures
Syntax, idioms & the data-structure patterns you actually reach for in problems.
1. Variables & Scalar Types
let x = 5; // immutable
let mut y = 5; // mutable
let z: i64 = 10; // explicit type
const MAX: u32 = 100; // compile-time constant
| Type | Notes |
i32 / i64 | signed ints (default i32) |
u32 / u64 | unsigned ints |
usize | index/length type — all .len() & indices |
f64 | float (use for division/floor) |
bool / char | true · 'a' (4-byte unicode) |
Warning: The #1 LeetCode trap: indices are
usizebut answers are usuallyi32. Cast freely withas:(end - start) as i32.
2. Casting & Number Tricks
let a = 7 as i64; // numeric cast
let h = (n as f64 / 2.0).floor() as i32; // floor division
let big = i32::MAX; // and i32::MIN
let q = 17 / 5; // 3 (integer div)
let r = 17 % 5; // 2 (mod)
let p = i64::pow(2, 10); // 1024
let ab = (-5).abs();
| Op | Meaning |
a.max(b) | larger of two |
a.min(b) | smaller of two |
std::cmp::min(a, b) | same, free-fn form |
a.saturating_sub(b) | subtract without underflow panic |
Warning: Underflow panics!
0usize - 1crashes. Guard loops withwhile right > left, or usechecked_sub/saturating_sub.
3. Strings & chars
String = owned/growable · &str = borrowed slice. You can't index a string by position — go through chars.
// the LeetCode move: convert to Vec\<char>
let v: Vec\<char> = s.chars().collect();
let n = s.len(); // bytes!
let n = s.chars().count(); // real char count
s.chars().nth(i) // Option\<char>, O(n)
v[i] // O(1) on the Vec
let mut out = String::new();
out.push('a');
out.push_str("bc");
out.trim_end().to_string()
Splitting & building
s.split_whitespace() // words
s.split(',')
v.iter().collect::\<String>() // chars → String
"x".repeat(3) // "xxx"
char tests & math
c.is_alphabetic(); c.is_numeric();
c.is_alphanumeric(); c.is_whitespace();
c.to_ascii_lowercase();
c.to_digit(10); // '7' → Some(7)
(c as u8 - b'a') as usize; // 'a'..'z' → 0..25
4. Vec — the workhorse
let mut v: Vec\<i32> = Vec::new();
let v = vec![1, 2, 3];
let v = vec![0; n]; // n zeros
// 2D grid (DP tables!)
let dp = vec![vec![0; cols]; rows];
| Method | Meaning |
v.push(x) | append |
v.pop() | Option\<T> off the end |
v.len() / v.is_empty() | size checks |
v.last() / v.first() | Option<&T> |
v.contains(&x) | linear search |
v.reverse() | in place |
v.swap(i, j) | swap two indices |
v[i..j] | slice (half-open) |
Sorting
v.sort(); // ascending
v.sort_by(|a, b| b.cmp(a)); // descending
v.sort_by_key(|x| x.0); // by field
v.sort_unstable(); // faster, no stable-tie guarantee
v.dedup(); // drop adjacent dups
Warning: Borrow in comparators:
a.cmp(&b),a.0.cmp(&b.0)— the closure hands you references.
5. Iterators (huge in Rust)
Chain adapters, then .collect() or fold to a value. Lazy until consumed.
v.iter() // &T (read)
v.iter_mut() // &mut T (edit in place)
v.into_iter() // T (consume / move)
| Adapter | Meaning |
.map(|x| x*2) | transform each |
.filter(|&x| x>0) | keep matching |
.sum() / .product() | annotate: .sum::\<i32>() |
.max() / .min() | → Option\<T> |
.count() | how many |
.rev() | reverse direction |
.enumerate() | → (index, val) |
.zip(other) | pair up two iters |
.position(|x| ..) | first matching index |
.any / .all(..) | → bool |
.fold(init, |a,x| ..) | accumulate |
let sum: i32 = v.iter().sum();
let sq: Vec\<i32> = v.iter().map(|x| x * x).collect();
let evens = v.iter().filter(|&&x| x % 2 == 0).count();
6. HashMap & HashSet
use std::collections::HashMap;
let mut m: HashMap<char, i32> = HashMap::new();
Frequency counting (the classic)
// idiom A — or_insert
*m.entry(c).or_insert(0) += 1;
// idiom B — and_modify
m.entry(c)
.and_modify(|v| *v += 1)
.or_insert(1);
| Method | Returns / effect |
m.get(&k) | Option<&V> |
m.get_mut(&k) | mutable ref to value |
m.contains_key(&k) | bool |
m.insert(k, v) | add / overwrite |
m.remove(&k) | delete |
m.keys() / m.values() | iterate |
m.len() / m.clear() | size / wipe |
HashSet — dedup & membership
use std::collections::HashSet;
let mut seen: HashSet\<i32> = HashSet::new();
if !seen.insert(x) { /* was already present — a dup! */ }
seen.contains(&x);
Tip: Iterating with edits: borrow first via
get_mut, or collect keys then mutate — you can't hold an iterator and mutate the map at the same time.
7. BinaryHeap (priority queue)
BinaryHeap is a max-heap by default. For a min-heap, wrap values in Reverse.
use std::collections::BinaryHeap;
use std::cmp::Reverse;
// max-heap
let mut h: BinaryHeap\<i32> = piles.into_iter().collect();
h.push(5);
while let Some(top) = h.pop() { /* ... */ }
// min-heap via Reverse
let mut mh: BinaryHeap<Reverse\<i32>> = BinaryHeap::new();
mh.push(Reverse(5));
if let Some(Reverse(min)) = mh.pop() { /* ... */ }
| Method | Meaning |
h.peek() | look at top, Option<&T> |
h.pop() | remove top |
h.len() | size |
Tip: For pairs in a heap,
(priority, item)sorts by the first field — order your tuple accordingly.
8. VecDeque (queue / BFS)
use std::collections::VecDeque;
let mut q: VecDeque\<i32> = VecDeque::new();
q.push_back(1); // enqueue
q.push_front(0);
q.pop_front(); // dequeue → Option
q.pop_back();
q.front(); q.back(); q.len();
Warning: BFS skeleton lives here — deque as the frontier,
HashSetfor visited.
while let Some(node) = q.pop_front() {
for nb in neighbors(node) {
if visited.insert(nb) {
q.push_back(nb);
}
}
}
9. Control Flow & Loops
if x > 0 { } else if x < 0 { } else { }
// if is an expression
let sign = if x >= 0 { 1 } else { -1 };
for i in 0..n { } // 0..n-1
for i in 0..=n { } // inclusive
for i in (0..n).rev() { } // reverse
for (i, x) in v.iter().enumerate() { }
while left < right { }
loop { break; } // infinite
match — pattern power
match x {
0 => "zero",
1 | 2 => "small",
3..=9 => "mid",
_ => "big", // catch-all required
}
10. Option & Result
Rust has no null. Maybe-a-value is Option\<T> = Some(x) / None.
if let Some(x) = v.last() { use(x); }
match m.get(&k) {
Some(v) => { },
None => { },
}
| Method | Meaning |
.unwrap() | get value or panic |
.unwrap_or(d) | value or default |
.unwrap_or_default() | value or 0 / "" / … |
.is_some() / .is_none() | presence checks |
.map(|x| ..) | transform if present |
Tip: On LeetCode
.unwrap()is usually fine once you've proven the value exists (e.g. after alen() > 0check).
11. Ownership & Borrowing
The part that fights you. Each value has one owner; you either move it, or borrow a reference.
| Form | Meaning |
&x | shared borrow (read, many allowed) |
&mut x | exclusive borrow (one at a time) |
x.clone() | deep copy — escape hatch when the borrow-checker bites |
// pass by ref → caller keeps ownership
fn sum(v: &Vec\<i32>) -> i32 {
v.iter().sum()
}
sum(&nums); // nums still usable after
Warning: Common errors:
- "borrowed after move" → take
&instead of moving, or.clone(). - "cannot borrow as mutable" → you already have another borrow alive; shorten its scope.
- Need two-pointer swaps? Index into the Vec rather than holding refs.
// swap via temp
let tmp = a.clone();
a = b;
b = tmp;
// or simply:
std::mem::swap(&mut a, &mut b);
12. Functions & Closures
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = return
}
fn noop() { } // returns ()
// nested fn (no captures) — handy in solutions
fn helper(v: &Vec\<i32>) -> i32 { /* .. */ }
// closures CAN capture environment
let k = 10;
let f = |x: i32| x + k;
let g = |a, b| a * b;
Tip: LeetCode method signatures use
pub fn name(&self, ...)insideimpl Solution. Theselfparam is just there — your logic goes in the body.
13. Pattern: Two Pointers & Sliding Window
// two pointers from both ends (palindrome / pair sum)
let (mut left, mut right) = (0usize, v.len() - 1);
while left < right {
if v[left] + v[right] == target { break; }
else if v[left] + v[right] < target { left += 1; }
else { right -= 1; }
}
// sliding window with a moving start
let (mut start, mut best) = (0usize, 0i32);
for end in 0..v.len() {
while window_invalid(start, end) { start += 1; }
best = best.max((end - start + 1) as i32);
}
Warning: Guard
right -= 1againstusizeunderflow whenrightcould be0. Thewhile left < rightcondition usually saves you.
14. Pattern: 2D DP Table
// e.g. Longest Common Subsequence — fill from bottom-right
let (a, b): (Vec\<char>, Vec\<char>) =
(s1.chars().collect(), s2.chars().collect());
let mut dp = vec![vec![0; b.len() + 1]; a.len() + 1];
for i in (0..a.len()).rev() {
for j in (0..b.len()).rev() {
dp[i][j] = if a[i] == b[j] {
dp[i + 1][j + 1] + 1
} else {
dp[i + 1][j].max(dp[i][j + 1])
};
}
}
// answer at dp[0][0]
Tip: Sizing the table
(n+1) × (m+1)with a zero border lets you readi+1/j+1without bounds checks. Iterate.rev()for the forward-looking recurrence above, or normal order for backward-looking ones.
15. Pattern: Stack (a plain Vec)
let mut stack: Vec\<char> = Vec::new();
for c in s.chars() {
if c == '(' {
stack.push(c);
} else if stack.last() == Some(&'(') {
stack.pop();
} else {
stack.push(c);
}
}
let leftover = stack.len() as i32;
Tip:
stack.last()returnsOption<&T>, so compare withSome(&val)— no need to.unwrap()and risk a panic on empty.
16. Top Gotchas & Fixes
| Problem | Fix |
| usize underflow | guard subtractions; use saturating_sub |
| type mismatch | index = usize, return = i32; cast with as |
| moved value | borrow & or .clone() |
| mut + immut borrow | split scopes; grab index not ref |
| can't index String | .chars().collect::<Vec<_>>() first |
| sum needs type | let s: i32 = it.sum(); |
| integer overflow | use i64 when sums get big |
| float floor | cast to f64, .floor(), cast back |
Debug printing
println!("{}", x); // Display
println!("{:?}", v); // Debug (Vec, tuple)
println!("{:#?}", m); // pretty Debug
eprintln!("err {}", e); // stderr
17. Pick-a-Structure
| Need | Reach for |
| ordered list, indexable | Vec\<T> |
| count / lookup by key | HashMap |
| seen-before / unique | HashSet |
| always pop max/min | BinaryHeap |
| FIFO queue / BFS | VecDeque |
| LIFO / matching | Vec as stack |
| fixed grid / DP | vec![vec![..]] |
| sorted unique keys | BTreeMap / BTreeSet |
cargo new prob · cargo run · cargo test
Part 2 — Algorithm Patterns & Templates
The skeletons — drop in the logic, change the condition, ship the solution.
1. Binary Search (the bounds)
Half-open [lo, hi) avoids most off-by-ones. mid computed to dodge overflow.
Exact match
fn search(v: &[i32], target: i32) -> i32 {
let (mut lo, mut hi) = (0usize, v.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if v[mid] == target {
return mid as i32;
} else if v[mid] < target {
lo = mid + 1;
} else {
hi = mid;
}
}
-1
}
Leftmost / lower bound
// first index where v[i] >= target
let (mut lo, mut hi) = (0usize, v.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if v[mid] < target { lo = mid + 1; }
else { hi = mid; }
}
// lo is the insertion point
Tip: Std shortcut:
v.binary_search(&t)→Ok(i)if found,Err(i)= where to insert. Alsopartition_point(|&x| x < t).
2. Binary Search on the Answer
When the question is "minimum X such that feasible(X)" — search the answer space, not the array.
let (mut lo, mut hi) = (min_ans, max_ans);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if feasible(mid) {
hi = mid; // try smaller
} else {
lo = mid + 1; // need bigger
}
}
// lo == smallest feasible answer
fn feasible(x: i64) -> bool {
// greedy check in O(n)
true
}
Tip: Classic uses: Koko eating bananas, split array largest sum, ship packages within D days. The trick is writing
feasible.
3. BFS (shortest path / levels)
use std::collections::{VecDeque, HashSet};
let mut q: VecDeque\<i32> = VecDeque::new();
let mut seen: HashSet\<i32> = HashSet::new();
q.push_back(start);
seen.insert(start);
let mut steps = 0;
while !q.is_empty() {
// process one whole level
let level_size = q.len();
for _ in 0..level_size {
let node = q.pop_front().unwrap();
if node == goal { return steps; }
for nb in neighbors(node) {
if seen.insert(nb) {
q.push_back(nb);
}
}
}
steps += 1;
}
Warning:
seen.insert(x)returnsfalseif already present — one call both checks and marks. Mark on enqueue, not dequeue, to avoid dups in the queue.
4. Grid Traversal (4-directional)
let (rows, cols) = (grid.len(), grid[0].len());
let dirs = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)];
for (dr, dc) in dirs {
let nr = r as i32 + dr;
let nc = c as i32 + dc;
// bounds check BEFORE casting back to usize
if nr >= 0 && nr < rows as i32
&& nc >= 0 && nc < cols as i32 {
let (nr, nc) = (nr as usize, nc as usize);
// visit grid[nr][nc]
}
}
Warning: The grid gotcha: compute neighbors as
i32so-1is representable, bounds-check, then cast tousize. Never subtract onusizecoordinates directly.
5. DFS (recursive)
Rust recursion needs all state passed or captured. A nested helper taking &mut state is the cleanest.
fn dfs(
node: usize,
adj: &Vec<Vec\<usize>>,
seen: &mut Vec\<bool>,
) {
seen[node] = true;
for &nb in &adj[node] {
if !seen[nb] {
dfs(nb, adj, seen);
}
}
}
// call:
let mut seen = vec![false; n];
dfs(0, &adj, &mut seen);
Tip: For grid DFS, pass
&mut gridand mark cells in place (flip to'0'/false) instead of a separate visited set.
6. Backtracking (subsets / perms)
Push → recurse → pop. The pop is the "undo" that makes it backtracking.
fn backtrack(
start: usize,
nums: &[i32],
path: &mut Vec\<i32>,
out: &mut Vec<Vec\<i32>>,
) {
out.push(path.clone()); // record
for i in start..nums.len() {
path.push(nums[i]); // choose
backtrack(i + 1, nums, path, out); // explore
path.pop(); // un-choose
}
}
let mut out = Vec::new();
backtrack(0, &nums, &mut Vec::new(), &mut out);
| Variant | Tweak |
| subsets | i+1, record every node |
| combinations | i+1, record at depth k |
| permutations | track used[]; start from 0 |
7. Union-Find (DSU)
Connectivity, cycle detection, counting components. Path compression + this layout is plenty fast.
struct Dsu { parent: Vec\<usize>, rank: Vec\<usize> }
impl Dsu {
fn new(n: usize) -> Self {
Dsu { parent: (0..n).collect(),
rank: vec![0; n] }
}
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
let root = self.find(self.parent[x]);
self.parent[x] = root; // compress
}
self.parent[x]
}
fn union(&mut self, a: usize, b: usize) -> bool {
let (ra, rb) = (self.find(a), self.find(b));
if ra == rb { return false; } // already joined
if self.rank[ra] < self.rank[rb] {
self.parent[ra] = rb;
} else if self.rank[ra] > self.rank[rb] {
self.parent[rb] = ra;
} else {
self.parent[rb] = ra;
self.rank[ra] += 1;
}
true
}
}
Tip:
unionreturningfalsemeans a cycle (both endpoints already in one set).
8. Prefix Sums
Precompute once → any range sum in O(1). Size n+1 with a leading 0 to avoid edge cases.
let mut pre = vec![0i64; nums.len() + 1];
for i in 0..nums.len() {
pre[i + 1] = pre[i] + nums[i] as i64;
}
// sum of nums[l..=r] (inclusive)
let range = pre[r + 1] - pre[l];
Subarray sum == k (hashmap trick)
use std::collections::HashMap;
let mut seen: HashMap<i64, i32> = HashMap::new();
seen.insert(0, 1); // empty prefix
let (mut sum, mut count) = (0i64, 0);
for &x in &nums {
sum += x as i64;
if let Some(&c) = seen.get(&(sum - k)) {
count += c;
}
*seen.entry(sum).or_insert(0) += 1;
}
9. Top-Down DP (memoization)
When the recurrence is natural but the table order isn't. Cache with a Vec sized to the state, sentinel -1 = "unsolved".
fn solve(i: usize, memo: &mut Vec\<i64>,
nums: &[i64]) -> i64 {
if i >= nums.len() { return 0; }
if memo[i] != -1 { return memo[i]; }
let take = nums[i]
+ solve(i + 2, memo, nums);
let skip = solve(i + 1, memo, nums);
memo[i] = take.max(skip);
memo[i]
}
// init: vec![-1; n]
Tip: 2D state →
vec![vec![-1; m]; n]. For HashMap-keyed states useHashMap<(usize, usize), i64>.
10. Monotonic Stack
"Next greater / smaller element" in O(n). Stack holds indices; pop while the new value breaks the order.
// next greater element to the right
let mut res = vec![-1; nums.len()];
let mut stack: Vec\<usize> = Vec::new();
for i in 0..nums.len() {
while let Some(&top) = stack.last() {
if nums[i] > nums[top] {
res[top] = nums[i];
stack.pop();
} else { break; }
}
stack.push(i);
}
Tip: Flip the comparison for next-smaller; iterate
.rev()for previous-element variants.
11. Fast / Slow & Greedy Notes
Cycle / midpoint (on arrays)
let (mut slow, mut fast) = (0usize, 0usize);
while fast + 1 < n {
slow += 1;
fast += 2;
}
// slow now at the midpoint
Greedy interval scheduling
// sort by end, take non-overlapping
intervals.sort_by_key(|iv| iv[1]);
let (mut end, mut count) = (i32::MIN, 0);
for iv in &intervals {
if iv[0] >= end {
count += 1;
end = iv[1];
}
}
Tip: Sort by end for "max non-overlapping"; by start for merging intervals.
12. Pick-a-Pattern (by signal)
| Problem says… | Try |
| sorted array, find X | binary search |
| "min/max … such that" | BS on answer |
| shortest path, unweighted | BFS |
| all paths / combos / perms | backtracking |
| connected groups / cycles | union-find |
| range sums repeatedly | prefix sums |
| next greater/smaller | monotonic stack |
| overlapping subproblems | DP (memo/table) |
| contiguous window | sliding window |
| top-k / streaming max | BinaryHeap |
Big-O sanity (n ≤ …)
| n ≤ | Affordable |
| 10–12 | O(n!) / O(2ⁿ) backtracking ok |
| 5,000 | O(n²) fine |
| 10⁶ | need O(n) or O(n log n) |
| 10⁹ | O(log n) — binary search |
Part 3 — The Bridge to Backend
From solving problems to building programs — traits, errors, types & shared state.
0. Where this fits
LeetCode Rust teaches the language core. This part is the layer between that and real services — the stuff puzzles never make you touch.
ownership → [traits & errors] → [smart pointers] → Arc\<Mutex> → async / Tokio → web framework
Warning: Mindset shift: on LeetCode you
.unwrap()freely. In a service,.unwrap()is how you crash. Real Rust threadsResulteverywhere and propagates with?.
1. Structs & methods
struct User {
id: u64,
name: String,
active: bool,
}
impl User {
// associated fn (constructor convention)
fn new(id: u64, name: String) -> Self {
Self { id, name, active: true }
}
// &self = borrow (read)
fn label(&self) -> String {
format!("#{}: {}", self.id, self.name)
}
// &mut self = borrow (mutate)
fn deactivate(&mut self) {
self.active = false;
}
}
let mut u = User::new(1, "Ada".into());
u.deactivate();
Tip:
Self= the type;self= the instance. Methods take&self,&mut self, orself(consumes).
2. Enums — model with variants
Rust enums carry data. This is how you model "one of several shapes" — the workhorse of domain modeling.
enum Event {
Click { x: i32, y: i32 }, // struct-like
Key(char), // tuple-like
Close, // unit
}
fn handle(e: Event) {
match e {
Event::Click { x, y } => { /* */ }
Event::Key(c) => { /* */ }
Event::Close => { /* */ }
}
}
Tip:
OptionandResultare just enums from std.matchmust cover every variant — the compiler enforces it.
3. Result & the ? operator
The heart of real Rust. Result<T, E> is Ok(T) or Err(E). ? unwraps Ok or returns the Err early.
fn parse_sum(a: &str, b: &str)
-> Result<i32, std::num::ParseIntError> {
let x: i32 = a.parse()?; // ? = unwrap or return Err
let y: i32 = b.parse()?;
Ok(x + y)
}
// handle at the boundary
match parse_sum("2", "3") {
Ok(n) => println!("{}", n),
Err(e) => eprintln!("bad input: {}", e),
}
| Tool | Meaning |
? | propagate error up the call stack |
.map_err(..) | convert one error type to another |
.ok() | Result → Option (drop error) |
.expect("msg") | unwrap with a custom panic message |
Warning: Real-world: crates like
anyhow(apps) andthiserror(libraries) make?work across many error types. Learn these once you're comfortable here.
4. Traits — shared behavior
Rust's core abstraction. A trait is a set of methods a type promises to provide — like an interface.
trait Shape {
fn area(&self) -> f64;
// default method (optional override)
fn describe(&self) -> String {
format!("area = {:.2}", self.area())
}
}
struct Circle { r: f64 }
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.r
}
}
let c = Circle { r: 2.0 };
println!("{}", c.describe());
Derive — free common traits
// auto-implement instead of hand-writing
#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }
// now: {:?} printing, .clone(), ==
| Derive | Enables |
Debug | {:?} printing |
Clone | .clone() |
PartialEq | == |
Default | ::default() |
5. Generics & trait bounds
Write code once for many types. Bounds say "any type T that implements these traits."
// T must be comparable (Ord)
fn largest<T: Ord + Copy>(v: &[T]) -> T {
let mut m = v[0];
for &x in v {
if x > m { m = x; }
}
m
}
// "where" clause for readability
fn show\<T>(item: T)
where T: std::fmt::Debug {
println!("{:?}", item);
}
// impl Trait — "some type that is..."
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
6. Smart pointers
These solve ownership problems plain references can't. Each answers a specific "but I need to…".
| Pointer | Use when |
Box\<T> | heap allocation; size known only at runtime / recursive types |
Rc\<T> | multiple owners, single-threaded (ref-counted) |
Arc\<T> | multiple owners, thread-safe (atomic count) |
RefCell\<T> | mutate through a shared ref; borrow checked at runtime |
Mutex\<T> | one-at-a-time mutable access across threads |
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // +1 owner, no deep copy
println!("{}", Rc::strong_count(&a)); // 2
// recursive type NEEDS Box for a known size
enum List { Cons(i32, Box\<List>), Nil }
Warning: The combos you'll actually use:
Rc<RefCell\<T>>for shared mutable state in one thread;Arc<Mutex\<T>>for the same across threads.
7. Arc<Mutex<T>> & threads
The thing about shared state. It exists because ownership forbids two threads mutating one value — so you share ownership (Arc) of a lock (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);
let h = thread::spawn(move || {
// lock() → guard; unlocks when dropped
let mut num = c.lock().unwrap();
*num += 1;
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 10
-
Arc= shareable ownership across threads. -
Mutex= guards the data;.lock()returns a guard that auto-unlocks on drop. -
movehands ownership of the clone into the closure.
Tip:
.lock().unwrap()is the conventional exception to "stop unwrapping" —.lock()only errors if another thread panicked while holding the lock (a "poisoned" mutex). In async backends you'll often usetokio::sync::Mutexinstead, so the lock can be held across an.await.
8. Send + Sync (the why)
Two marker traits the compiler uses to make concurrency safe. You rarely write them — you just satisfy them.
| Trait | Meaning |
Send | safe to move to another thread |
Sync | safe to share (&T) across threads |
This is why Rc won't compile across threads but Arc will: Rc isn't Send. The compiler catches data races before they exist — the error message tells you exactly which bound is missing.
Warning: Don't memorize these. Just know: if the compiler says "
X cannot be sent between threads safely," you usually needArc/Mutexinstead ofRc/RefCell.
9. Modules, crates & Cargo
# start a project / add a dependency
cargo new my_api
cargo add serde --features derive
cargo add tokio --features full
// modules organize code
mod db {
pub fn connect() { /* */ }
pub struct Pool;
}
use db::Pool; // bring into scope
db::connect(); // or call fully-qualified
| Keyword | Meaning |
pub | makes an item visible outside its module |
mod x; | loads x.rs / x/mod.rs |
crate:: | path from the crate root |
super:: | path from the parent module |
10. async / await (preview)
Backend frameworks are async-first. An async fn returns a future that does nothing until .awaited by a runtime (Tokio).
async fn fetch(id: u64) -> Result<String, String> {
// ... awaits a DB / network call
Ok(format!("user {}", id))
}
#[tokio::main]
async fn main() {
let user = fetch(1).await.unwrap();
println!("{}", user);
}
-
.awaityields control while waiting — doesn't block the thread. -
#[tokio::main]sets up the runtime that drives futures. -
Common stack: axum (web) + sqlx (DB) + serde (JSON).
11. Suggested order from here
| Step | Focus |
| 1 | Solidify structs, enums, match by modeling small domains |
| 2 | Convert .unwrap() habits → Result + ? everywhere |
| 3 | Traits + generics; derive the common ones |
| 4 | Smart pointers; build something with Rc\<RefCell> |
| 5 | Arc\<Mutex> + thread::spawn for shared state |
| 6 | async / await + Tokio basics |
| 7 | Build a tiny JSON API with axum |
Free, high-quality resources
-
The Book — doc.rust-lang.org/book (chapters 10, 13, 15–16 map to this part)
-
Rust by Example — runnable snippets
-
Tokio tutorial — when you hit async
All code in this reference was compiled and verified on the Rust 2021 edition. Snippets labeled "preview" or "skeleton" (async, feasible, neighbors) are templates to fill in, not complete runnable programs.