Skip to main content
Current1mo ago

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

  1. Syntax & Data Structures

  2. Algorithm Patterns & Templates

  3. The Bridge to Backend


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

TypeNotes
i32 / i64signed ints (default i32)
u32 / u64unsigned ints
usizeindex/length type — all .len() & indices
f64float (use for division/floor)
bool / chartrue · 'a' (4-byte unicode)

Warning: The #1 LeetCode trap: indices are usize but answers are usually i32. Cast freely with as: (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();

OpMeaning
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 - 1 crashes. Guard loops with while right > left, or use checked_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];

MethodMeaning
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)

AdapterMeaning
.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);

MethodReturns / 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() { /* ... */ }

MethodMeaning
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, HashSet for 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 => { },
}

MethodMeaning
.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 a len() > 0 check).

11. Ownership & Borrowing

The part that fights you. Each value has one owner; you either move it, or borrow a reference.

FormMeaning
&xshared borrow (read, many allowed)
&mut xexclusive 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, ...) inside impl Solution. The self param 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 -= 1 against usize underflow when right could be 0. The while left < right condition 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 read i+1 / j+1 without 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() returns Option<&T>, so compare with Some(&val) — no need to .unwrap() and risk a panic on empty.

16. Top Gotchas & Fixes

ProblemFix
usize underflowguard subtractions; use saturating_sub
type mismatchindex = usize, return = i32; cast with as
moved valueborrow & or .clone()
mut + immut borrowsplit scopes; grab index not ref
can't index String.chars().collect::<Vec<_>>() first
sum needs typelet s: i32 = it.sum();
integer overflowuse i64 when sums get big
float floorcast 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

NeedReach for
ordered list, indexableVec\<T>
count / lookup by keyHashMap
seen-before / uniqueHashSet
always pop max/minBinaryHeap
FIFO queue / BFSVecDeque
LIFO / matchingVec as stack
fixed grid / DPvec![vec![..]]
sorted unique keysBTreeMap / 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. Also partition_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) returns false if 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 i32 so -1 is representable, bounds-check, then cast to usize. Never subtract on usize coordinates 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 grid and 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);

VariantTweak
subsetsi+1, record every node
combinationsi+1, record at depth k
permutationstrack 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: union returning false means 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 use HashMap<(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 Xbinary search
"min/max … such that"BS on answer
shortest path, unweightedBFS
all paths / combos / permsbacktracking
connected groups / cyclesunion-find
range sums repeatedlyprefix sums
next greater/smallermonotonic stack
overlapping subproblemsDP (memo/table)
contiguous windowsliding window
top-k / streaming maxBinaryHeap

Big-O sanity (n ≤ …)

n ≤Affordable
10–12O(n!) / O(2ⁿ) backtracking ok
5,000O(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 threads Result everywhere 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, or self (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: Option and Result are just enums from std. match must 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),
}

ToolMeaning
?propagate error up the call stack
.map_err(..)convert one error type to another
.ok()ResultOption (drop error)
.expect("msg")unwrap with a custom panic message

Warning: Real-world: crates like anyhow (apps) and thiserror (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(), ==

DeriveEnables
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…".

PointerUse 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.

  • move hands 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 use tokio::sync::Mutex instead, 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.

TraitMeaning
Sendsafe to move to another thread
Syncsafe 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 need Arc / Mutex instead of Rc / 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

KeywordMeaning
pubmakes 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);
}

  • .await yields 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

StepFocus
1Solidify structs, enums, match by modeling small domains
2Convert .unwrap() habits → Result + ? everywhere
3Traits + generics; derive the common ones
4Smart pointers; build something with Rc\<RefCell>
5Arc\<Mutex> + thread::spawn for shared state
6async / await + Tokio basics
7Build 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.