◆ The complete beginner's handbook

Learn Rust from absolute zero

A visual, deep-dive course that takes you from "what is a variable?" all the way to ownership, lifetimes, traits, async concurrency — and honest, measured performance comparisons against C, C++, Go, Java, Python and JavaScript.

21
In-depth chapters, from println! to FFI
150+
Annotated, runnable Rust code examples
40+
Diagrams, charts & visual explainers
0
Garbage collector pauses. Ever.
"Rust is the first language in decades that makes systems programming feel safe. It doesn't ask you to choose between speed and correctness — it hands you both, and makes the compiler prove it." — why millions of developers keep voting Rust "most loved language" year after year
Chapter 01

What is Rust, and why should you care?

Rust is a systems programming language that runs close to the metal, has no garbage collector, compiles to native machine code — and yet guarantees memory safety at compile time. That combination was considered impossible for 40 years.

Let's start at the very beginning. Every program you run is ultimately a set of instructions for a CPU and a set of bytes in memory. Most languages sit far above that reality: Python, JavaScript and Java run inside an interpreter or virtual machine that manages memory for you (a garbage collector, or GC). That's wonderfully convenient — and it costs you speed, memory, and predictable timing. C and C++ sit right on the metal and are blazing fast, but they hand you the responsibility of never freeing memory twice, never writing past the end of an array, and never letting two threads touch the same variable at once. Get it wrong and your program leaks, crashes, or gets exploited by an attacker.

Rust refuses that trade-off. It gives you C-level control over memory layout, allocation and instructions, while a strictly-typed compiler proves — before your program ever runs — that you have no use-after-free, no double-free, no data races, and no null pointer dereferences. If it compiles, an entire class of the world's most damaging bugs simply cannot exist.

The three pillars of Rust

🛡️

Safety

No null, no dangling pointers, no buffer overflows, no data races — all rejected at compile time. Memory is freed deterministically the moment a value goes out of scope, via Drop.

⚡

Speed

Zero-cost abstractions: high-level expressiveness that compiles down to the same assembly a C programmer would write by hand. No runtime, no GC pauses, no hidden allocations.

🧵

Fearless concurrency

The type system knows which values can safely cross thread boundaries (Send / Sync). The compiler rejects data races the same way it rejects type errors.

💡 The one-sentence version

Rust is "C++ but the compiler refuses to let you shoot yourself in the foot, and doesn't charge you for the privilege." It's a language built around a single idea: you should not have to choose between control and correctness.

A short history

2006

A side project at Mozilla

Graydon Hoare starts designing Rust as a personal project, frustrated by crashes caused by memory errors in browsers. The name comes from a group of fungi — puccinia rust — known for being stubbornly hard to eradicate.

2009–2012

Mozilla adopts it for Servo

Mozilla sponsors the language officially and builds the Servo browser engine to prove Rust could replace C++ in performance-critical infrastructure. The famous borrow checker arrives during this era and reshapes the whole design.

May 15, 2015

Rust 1.0 — stability guaranteed

The first stable release. From this day forward, code written for Rust 1.0 still compiles on modern Rust. A strict backwards-compatibility promise that the project has kept ever since.

2018 · 2021 · 2024

Editions keep the language evolving

Rust uses editions — opt-in language versions — so new ergonomics can land without breaking old code. Different crates in one project can even use different editions.

2021–2022

The industry switches on

Rust enters the Linux kernel, Android, Windows core libraries, and AWS's firecracker. Rust Foundation founded by Mozilla, AWS, Google, Huawei, Microsoft. From "interesting experiment" to critical infrastructure.

Today

Everywhere performance and reliability matter

Operating systems, browsers, game engines, embedded devices, WebAssembly, CLI tools, databases (ClickHouse, SurrealDB, TiKV), proxies (Cloudflare Pingora), and half the tools developers use daily: ripgrep, fd, bat, exa, diesel, tokio.

Where Rust sits in the language landscape

Every language lives somewhere on two axes: how close it runs to the hardware (raw performance and control) and how much the language protects you (safety, ergonomics, abstraction). Rust's whole thesis is that these two axes should not be a trade-off.

↑ more protection
more raw control →
Performance & hardware control
Safety & abstraction
Rust
C++
C
Zig
Go
Java
C#
Swift
OCaml
Python
JavaScript
Ruby

Illustrative positioning, not a measurement. C and C++ sit at maximum control but low built-in protection; managed languages sit high on protection but pay a runtime cost. Rust is deliberately alone in the top-right corner.

What makes Rust's guarantee technically possible

🧠

Ownership system

Every value has exactly one owner. When the owner goes out of scope, the value is dropped (freed). No reference counting in most code, no GC, no manual free().

🔗

Borrow checker

References must always be valid. At any moment you may have either one mutable reference or any number of shared references — never both. This is what kills data races at compile time.

🧩

No null references

Tony Hoare called null references his "billion-dollar mistake". Rust has no null. Absence is expressed explicitly with Option<T>, and the compiler forces you to handle the "nothing" case.

📦

Algebraic types & exhaustiveness

enum can carry data, and match must cover every possible case. Add a new variant and the compiler points at every place that must now deal with it.

🧊

Deterministic destruction (RAII)

Resources are released at a known point in the program, not whenever a GC feels like it. Files close, locks unlock, memory frees — exactly at the end of the scope.

⚙️

Zero-cost abstractions

Generics are monomorphized (specialised per type at compile time), iterators inline into loops, and closures become plain function pointers. "What you don't use, you don't pay for. What you do use, you couldn't hand-code any better."

Rust in the real world

Firefox / Servo Linux kernel modules Android (system components) Windows (core libraries) Cloudflare Pingora Discord AWS Firecracker / Nitro npm registry Figma multiplayer backend 1Password Dropbox file sync ClickHouse / Meilisearch Polars / DataFusion Embassy (embedded) Bevy game engine Solana / Substrate (blockchain) ripgrep · fd · bat · zoxide Tailscale · Deno · Turbopack
🧩 Myth-busting: "Rust is hard"

Rust is strict, not hard. The difficulty is front-loaded: the first few weeks feel like arguing with a very opinionated compiler. But those rules are exactly the rules you were silently violating in C++ or debugging for days in Python. After the mental model clicks, most developers report that Rust code works the first time it compiles, and the compiler becomes the best documentation they've ever had.

Compare: C++ is "easy to write, extremely hard to write correctly". Rust is "hard to write for 3 weeks, then dramatically easier to keep correct forever."

How Rust compares at a glance

PropertyRustC / C++GoJava / C#Python / JS
Memory managementOwnership + Drop, deterministicManual (malloc/free, RAII)Garbage collectorGarbage collectorReference counting / GC
Null pointersImpossible (Option<T>)UbiquitousZero-values + nilnull everywherenull/undefined
Data racesCompile errorUndefined behaviourRuntime detector (race detector)Runtime (locks required)GIL / event loop hides it
Typical speedNative, ~C++ levelNative, fastest tierFast (≈1.5–2.5× C)Fast after JIT warm-up10–100× slower
Startup timeMillisecondsMillisecondsMilliseconds100s of ms (JVM warm-up)Fast, but slow execution
Runtime / VMNoneNoneSmall runtime + schedulerFull VM / CLRInterpreter / V8
Compile speedSlow (incremental is good)Slow (C++ headers)Very fastModerateNo compilation
Concurrency modelThreads + async/await, typedThreads (raw), no safetyGoroutines + channelsThreads, virtual threadsAsync / single-threaded
Error handlingResult + ?, typedExceptions / error codesMultiple return valuesExceptionsExceptions
Package managercargo + crates.ioFragmentedgo modulesMaven / NuGetpip / npm
Learning curveSteep first 3 weeksModerate to learn, hard to masterGentleModerateGentle
Binary size (hello world)~300 KB–2 MB~10–30 KB (C)~7 MBRequires runtimeRequires interpreter
Table values are typical, order-of-magnitude comparisons. Details and full benchmarks arrive in Chapter 17.
Chapter 02

Installing Rust & your first program

Rust ships with one of the best toolchains in any language: rustup (installer/updater), rustc (the compiler), and cargo (build system, package manager, test runner and task runner — all in one).

Step 1 — install

On Linux or macOS, open a terminal and run:

terminal
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

On Windows, download rustup-init.exe from rustup.rs — it will also install the Visual Studio C++ Build Tools if needed. Then verify everything works:

terminal
rustc --version
cargo --version
rustup update          # upgrade to the latest stable, anytime
rustup default stable  # or: stable / beta / nightly
rustup component add clippy rustfmt rust-src
🧭 The toolchain, in one paragraph

You will almost never call rustc directly. cargo is the command you live in — it calls rustc for you, tracks dependencies, resolves versions, and keeps build artifacts in a target/ folder so rebuilds are incremental and fast. rustup manages the toolchain versions themselves (stable / beta / nightly, per-project if needed).

Step 2 — hello, world

Create a project. This makes a folder with a git repo, a Cargo.toml manifest, and a src/ directory:

terminal
cargo new hello_rust
cd hello_rust
cargo run

That last line compiles and runs the program, printing Hello, world!. Now let's understand every character of that file.

src/main.rs
fn main() {
    println!("Hello, world!");
}
1 · fn Declares a function. fn name(params) -> RetType { body }. Every executable Rust program starts at the function called main — no exceptions.
→
2 · main() The entry point. No parameters, and it returns nothing here (implicitly (), the "unit" type — Rust's version of void).
→
3 · { } Braces define a block scope. Indentation is not significant (Rust style is 4 spaces, enforced by rustfmt).
→
4 · println! The ! means this is a macro, not a function. Macros generate code at compile time. println! formats text and writes a line to standard output.
→
5 · ; Statements end with a semicolon. A block's final expression (with no semicolon) is its return value — a rule we'll exploit constantly.

Step 3 — how a cargo project is laid out

project tree
hello_rust/
├── Cargo.toml        # the manifest: name, version, dependencies, metadata
├── Cargo.lock        # exact resolved versions (commit this for binaries!)
├── .gitignore        # ignores target/
├── src/
│   ├── main.rs       # binary crate root — this is your program
│   └── lib.rs        # (optional) library crate root
├── tests/            # integration tests (black-box, one file = one test binary)
├── examples/         # small runnable examples: cargo run --example foo
├── benches/          # benchmarks (criterion)
└── target/           # ALL build output. never edit, safe to delete
Cargo.toml
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"          # language version: 2015 | 2018 | 2021 | 2024

[dependencies]
serde = { version = "1.0", features = ["derive"] }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"         # only used by tests & benchmarks

[profile.release]
opt-level = 3             # maximum optimisation
lto = true                # link-time optimisation
codegen-units = 1
panic = "abort"

Every cargo command you'll actually use

CommandWhat it doesWhen you use it
cargo new nameCreates a new binary projectStarting something
cargo initInitialises cargo in an existing folderAdopting cargo
cargo buildCompiles to target/debugChecking it compiles
cargo build --releaseCompiles optimised, to target/releaseShipping / benchmarking
cargo runBuild + executeEvery 30 seconds, constantly
cargo checkType-checks without code generation (much faster)The inner loop while coding
cargo testRuns unit, integration and doc testsAfter every change
cargo doc --openBuilds & opens local docs for your depsLearning an API offline
cargo clippy500+ lints — the "senior dev" reviewing your codeBefore every commit
cargo fmtFormats all code to the official styleBefore every commit
cargo add crateAdds a dependency to Cargo.tomlPulling in a library
cargo updateUpdates Cargo.lock within semver limitsRefreshing deps
cargo benchRuns benchmarksMeasuring performance
cargo treePrints the dependency graphDebugging bloat / conflicts
cargo expandShows what your macros generateDebugging macros / derive
cargo cleanDeletes target/Rarely — when something is cursed

Make it interactive

src/main.rs
use std::io;   // bring the I/O module into scope

fn main() {
    println!("What is your name?");

    let mut name = String::new();          // a growable, heap-allocated string

    io::stdin()
        .read_line(&mut name)              // &mut = "please fill THIS in"
        .expect("failed to read line");    // handle the error (see Chapter 09)

    let name = name.trim();                // shadow the variable with a cleaned version

    println!("Hello, {name}! Welcome to Rust.");  // inline variable interpolation
}
💡 Try it without installing anything

The Rust Playground runs Rust entirely in your browser — including compiling with Release mode, viewing the generated assembly, and running benchmarks. Perfect for experimenting with every example in this handbook.

Set up your editor (do this once)

🆚

VS Code

Install the rust-analyzer extension. You get inline types, "implement missing trait" quick-fixes, borrow-checker hints, and go-to-definition across crates.

🌙

Neovim / Helix / Emacs

Use the rust-analyzer language server binary directly (installed via rustup component add rust-analyzer) with your LSP client.

🦀

RustRover (JetBrains)

A full IDE dedicated to Rust with first-class refactoring, macro expansion and debugger support. Free for non-commercial use.

Chapter 03

Variables, mutability & data types

Rust variables are immutable by default. This single decision eliminates entire categories of bugs and lets the compiler optimise aggressively — and you opt in to change with one keyword.

let, mut, const — and why nothing is mutable by default

variables.rs
fn main() {
    let x = 5;              // immutable: that's it, x is 5 forever
    // x = 6;               // ❌ compile error E0384: cannot assign twice

    let mut y = 5;          // explicitly mutable
    y = 6;                  // ✅ fine
    println!("y = {y}");    // y = 6

    // constants: ALWAYS immutable, must be ANNOTATED, chosen at compile time,
    // may be declared anywhere (even at module scope), conventionally SCREAMING_CASE.
    const MAX_POINTS: u32 = 100_000;
    const SECONDS_IN_DAY: u64 = 24 * 60 * 60;
    println!("{MAX_POINTS} / {SECONDS_IN_DAY}");

    // static: like const, but has a fixed memory address and can implement Sync.
    static GREETING: &str = "hello from a static";

    let spaces = "   ";     // type &str (a string slice) — inferred from the literal
    let mut spaces = spaces.len(); // ⚠️ this is a NEW variable (shadowing), type usize
    spaces += 1;
    println!("{spaces}");
}

Shadowing is not the same as mut

Writing let x = ... a second time does not change the old variable — it creates a brand new one that shadows the previous name. This is extremely common and useful:

shadowing.rs
fn main() {
    let input = "  42  ";
    let input = input.trim();        // now a &str without whitespace
    let input: u32 = input.parse().expect("not a number");  // now a number!

    println!("{input} + 1 = {}", input + 1);   // 42 + 1 = 43

    // Shadowing lets you CHANGE TYPE. `mut` does not:
    let spaces = "   ";
    // let spaces = spaces.len();   // ✅ allowed: new variable, new type
    let mut sp = "   ";
    // sp = sp.len();               // ❌ E0308: expected &str, found usize
}
✓ Use mut when…
  • You genuinely need to modify the same value in place
  • You're accumulating into a Vec, counter, or buffer
  • The value's type stays the same
✓ Use shadowing when…
  • You're transforming a value (trim, parse, convert)
  • You want to keep the original immutable
  • You need to change the type of a name

The primitive types

Integers — sized explicitly, never ambiguous

TypeBitsRange (signed)Typical use
i8 / u88−128…127 / 0…255Bytes, raw data, RGB values
i16 / u1616±32 768 / 0…65 535Audio samples, ports
i32 / u3232±2.1 billion / 0…4.29 billionDefault integer type
i64 / u6464±9.2×10¹⁸ / 0…1.8×10¹⁹Timestamps, large counters, IDs
i128 / u128128enormousCrypto, big fixed-point maths
isize / usize32 or 64platform pointer sizeArray indices, lengths, allocations
Underscores improve readability and are ignored: 1_000_000. Literals may carry a suffix: 92u8, 3.14f64, 0xFFu32, 0b1010, 0o777.

Floats, booleans & characters

primitives.rs
fn main() {
    // Floating point: f64 is the default (same speed as f32 on modern CPUs, twice precision)
    let pi = 3.141592653589793;     // f64
    let small: f32 = 2.5;           // explicit f32 when memory matters (GPU, embedded)
    println!("{} {}", pi, 1.0_f64.sqrt());

    // Booleans are exactly 1 byte
    let is_rust_fun: bool = true;
    let nope = false;

    // char is a 4-byte Unicode scalar value — NOT a byte!
    let letter = 'A';
    let emoji = '🦀';               // yes, a char
    let heart = '❤';
    println!("{letter} {emoji} {heart} — all chars are 4 bytes");

    // Arithmetic and explicit casting with `as`
    let a: i32 = 7;
    let b: i32 = -3;
    println!("{} {} {} {}", a + b, a - b, a * b, a / b);  // 4 10 21 -2 (integer division!)
    println!("remainder: {}", a % b);                      // 1
    println!("7.0 / 2.0 = {}", 7.0 / 2.0);                 // 3.5 (floats divide normally)
    println!("cast: {}", a as f64 / 2.0);                  // 3.5
    println!("bool as int: {}", is_rust_fun as i32);       // 1
}
⚠️ Integer overflow: what actually happens

In a debug build (cargo build), overflowing an integer panics — your program stops with a clear message. In a release build (--release), it wraps around silently (255 + 1 → 0 for u8), matching C behaviour and enabling maximum performance. This difference surprises people — always test both.

When you want defined behaviour in every profile, use the checked/wrapping/saturating APIs: x.checked_add(1) → Option, x.wrapping_add(1) → always wraps, x.saturating_add(1) → clamps at the limit, x.overflowing_add(1) → (value, did_overflow).

Compound types

Tuples — fixed-size groups of mixed types

tuples.rs
fn main() {
    let point: (i32, f64, char) = (10, 2.5, 'x');

    // Destructure with a pattern:
    let (x, y, z) = point;
    println!("{x} {y} {z}");

    // Or access with zero-based dot indices:
    println!("{} {}", point.0, point.2);

    // The empty tuple () is the "unit type" — it means "no meaningful value"
    let nothing: () = ();
    println!("() occupies {} bytes", std::mem::size_of_val(&nothing)); // 0 bytes

    // A 1-tuple needs a trailing comma:
    let one = (42,);
    println!("{:?}", one);
}

Arrays vs slices vs Vec — know the difference

arrays.rs
fn main() {
    // ARRAY: fixed length, lives on the stack, one contiguous block, type [T; N]
    let months: [&str; 12] = ["Jan","Feb","Mar","Apr","May","Jun",
                              "Jul","Aug","Sep","Oct","Nov","Dec"];
    let zeros = [0; 5];            // [0, 0, 0, 0, 0] — repeat initialiser
    println!("{months:?}  {zeros:?}");

    // Indexing is CHECKED at runtime — out of bounds panics instead of corrupting memory
    println!("{}", months[2]);     // "Mar"
    // println!("{}", months[99]); // 💥 panic: index out of bounds: len is 12, index is 99

    // Prefer .get() when the index might be invalid — it returns Option<&T>, never panics
    match months.get(99) {
        Some(m) => println!("{m}"),
        None    => println!("no month at that index"),
    }

    // SLICE: a *view* into part of an array/string — just (pointer, length), 2 words
    let slice = &months[1..4];     // ["Feb", "Mar", "Apr"]
    println!("{slice:?}  len={}", slice.len());
    println!("size of the slice handle itself: {}", std::mem::size_of_val(&slice)); // 16 on 64-bit (ptr + length)

    // Vec<T>: heap-allocated, growable. See Chapter 08.
    let mut nums: Vec<i32> = vec![1, 2, 3];
    nums.push(4);
    println!("{nums:?}");
}
Where each collection lives in memory
Stack — fast, automatic, fixed size
x: i324 bytes
flag: bool1 byte
c: char4 bytes
months: [&str; 12]12 × 16 bytes
slice: &[i32]16 bytes (ptr + len)
Pushed/popped automatically when a function is called/returns. Extremely fast — just moving the stack pointer. Size must be known at compile time.
Heap — flexible, slower, explicitly managed
s: String (on stack)24 bytes: ptr, len, cap
↓ points to
"hello" bytesheap allocation
nums: Vec<i32> (stack)24 bytes
↓ points to
[1, 2, 3, 4]heap allocation
Size can grow at runtime. Allocation is a real cost, so Rust lets you control exactly when it happens — unlike GC languages where the runtime allocates behind your back.

Key insight: String and Vec<T> are fat handles that live on the stack and point at data on the heap. That's why passing them around brings ownership questions — which is exactly what Chapter 05 solves.

Type inference & annotations

Rust is statically and strongly typed, but it infers most types so you rarely write them. You only annotate when:

  • Declaring const or static items (always required)
  • Function parameters and return types (always required — this is a deliberate API design choice)
  • The type can't be deduced: let x: u64 = "42".parse().unwrap();
  • You want to pick a specific numeric type: let n: u8 = 200;
  • Using the turbofish: let v: Vec<i32> = (0..5).collect(); or .collect::<Vec<i32>>()
🚫 Integer division truncates

7 / 2 equals 3, not 3.5 — both operands are integers. To get a float, cast explicitly: 7.0 / 2.0 or 7 as f64 / 2.0. This catches beginners constantly.

Chapter 04

Functions & control flow

Rust is an expression-oriented language. Almost everything returns a value — including if, match, and plain blocks { }. Once this clicks, Rust code becomes remarkably elegant.

Function anatomy

functions.rs
//   ┌── fn keyword      ┌─ parameter: name: Type (ANNOTATIONS REQUIRED)
//   │                   │
fn  add(a: i32, b: i32) -> i32 {
    a + b                // ← no semicolon: this is an EXPRESSION, returned automatically
}

fn shout(text: &str) -> String {   // return type after the arrow
    format!("{text}!!!")            // last expression is the return value
}

fn do_something() {                 // no -> means it returns ()  (the unit type)
    println!("side effect only");
    // an implicit `; ()` is appended here
}

fn early_return(x: i32) -> i32 {
    if x < 0 {
        return 0;        // explicit early return with `return` keyword
    }
    x * 2                // tail expression
}

// Never returns (diverging function) — used for panic!, exit(), infinite loops
fn never() -> ! {
    panic!("this function never returns a value")
}

fn main() {
    println!("{}", add(2, 3));        // 5
    println!("{}", shout("hello"));   // hello!!!
    do_something();
    println!("{}", early_return(-7)); // 0
}

Statements vs expressions — the rule that trips everyone up

expressions.rs
fn main() {
    // A STATEMENT performs an action and returns () — it does NOT return a value
    let y = 6;                          // statement

    // AN EXPRESSION evaluates to a value
    let z = {                           // a block is an expression!
        let inner = 3 * 2;
        inner + 1                       // ← no semicolon → this block evaluates to 7
    };
    println!("z = {z}");                // 7

    // Adding a semicolon turns an expression into a statement returning ()
    let w = { 5; };                     // type is () — the 5 is thrown away
    println!("w = {w:?}");              // ()

    // ❌ This is the classic beginner error:
    // let bad = (let x = 5);          // error: `let` is a statement, not an expression
}

if / else as an expression

conditionals.rs
fn main() {
    let temperature = 28;

    // Conditions MUST be bool — Rust never coerces numbers/strings to boolean
    if temperature > 25 {
        println!("hot");
    } else if temperature > 10 {
        println!("pleasant");
    } else {
        println!("cold");
    }
    // if temperature { }  // ❌ E0308: expected `bool`, found integer

    // Use `if` as an expression to choose a value — no ternary operator needed
    let mood = if temperature > 25 { "melting" } else { "fine" };
    println!("{mood}");

    // Both branches must have the SAME type
    let n = if temperature > 25 { 10 } else { "ten" };  // ❌ E0308: mismatched types
}

Loops — three flavours, all with a twist

loops.rs
fn main() {
    // ── loop: repeats forever… but can RETURN A VALUE ────────────────────
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;      // break with a value → the loop evaluates to 20
        }
    };
    println!("result = {result}");  // 20

    // ── while: conditional looping ───────────────────────────────────────
    let mut n = 3;
    while n != 0 {
        println!("{n}!");
        n -= 1;
    }
    println!("LIFTOFF 🚀");

    // ── for: iterate over anything (ranges, arrays, iterators) ───────────
    for i in 0..4 {                 // 0,1,2,3  (end is exclusive)
        print!("{i} ");
    }
    println!();
    for i in 0..=4 {                // 0,1,2,3,4  (inclusive)
        print!("{i} ");
    }
    println!();
    for (index, letter) in ["a","b","c"].iter().enumerate() {
        print!("[{index}]={letter} ");
    }
    println!();

    // for loops over ITERATORS are the idiomatic Rust way (and they're optimised
    // to exactly the same machine code as the equivalent `while` loop)
    let sum: i32 = (1..=100).sum();
    println!("sum 1..=100 = {sum}");   // 5050

    // ── labelled breaks: escape nested loops ────────────────────────────
    'outer: for x in 0..5 {
        for y in 0..5 {
            if x + y == 6 { break 'outer; }   // exits BOTH loops
            if y == 2 { continue 'outer; }    // continues the OUTER loop
        }
    }
}
🔁

loop

Infinite by design. The only way to return a value from a loop. Compilers can't always prove a while terminates, but they know loop is intentional.

⏲️

while

Standard conditional loop. Note: Rust has no do...while — use loop { ...; if !cond { break } }.

🎯

for

Always prefer this. It consumes an iterator, is bounds-safe, and cannot produce an off-by-one bug the way C's for(i=0;i<=n;i++) can.

💡 Performance note

A for loop over an iterator is not slower than a manual index loop — in release builds LLVM inlines the iterator calls and produces identical machine code, usually faster because bounds checks are eliminated when the compiler can prove the index is safe. This is the "zero-cost abstraction" principle in action.

Chapter 05

Ownership & borrowing — the heart of Rust

This is the chapter that makes Rust Rust. If you understand ownership and borrowing, you understand 80% of what makes the language unique. Read it slowly. Re-read it. Everyone does.

The problem ownership solves

Every program needs to decide who frees memory, and when. History has given us exactly two mainstream answers, and both are flawed:

✋

Manual (C / C++)

Maximum speed, maximum risk. You call malloc and free yourself. Forget one → memory leak. Free twice → heap corruption. Use after freeing → security hole exploited by attackers. These are the #1 cause of critical CVEs.

🧹

Garbage collection (Java, Go, Python, JS)

Convenient, costly. A background process finds unused memory. But it consumes 10–30% of your CPU and RAM, causes unpredictable latency pauses at the worst possible moment, and still doesn't prevent logic bugs or data races.

🦀

Ownership (Rust)

Fast and safe. A set of three compile-time rules determines exactly when each value is freed. No runtime cost. No leaks. No double-free. No pauses. The compiler checks it all before the program exists.

The three rules of ownership

Rule 1 Every value has exactly one owner — a variable. Not two, not zero.
→
Rule 2 There can only be one owner at a time. When the owner changes, the old one is invalidated.
→
Rule 3 When the owner goes out of scope, the value is dropped (freed) automatically.

Rule 3 in action: deterministic destruction

drop.rs
fn main() {
    {                                   // s is not yet valid
        let s = String::from("hello");  // s is valid from here onward
        println!("{s}");                // use s — it's alive
    }                                   // ← scope ends: `drop(s)` is called HERE automatically
    // s is no longer valid — trying to use it is a compile error

    println!("s is gone, and the memory was already freed.");
}

The call to drop happens at a precise, known point in your program — the closing brace. Not "sometime later when the GC runs". This predictability is why Rust is used for real-time audio, game engines, and operating systems.

Rule 2 in action: move semantics

Here is the moment most beginners stare at the screen. Some values (like numbers) can be trivially duplicated; others (like String) own a chunk of heap memory and cannot.

✅ Simple values — copied freely
let a = 5;
let b = a;      // copy
println!("{a} {b}");  // both valid

i32 has the Copy trait. Its size is known and it lives entirely on the stack, so assigning it makes a cheap bitwise duplicate. Both variables remain usable.

❌ Heap-owning values — moved, not copied
let s1 = String::from("hello");
let s2 = s1;          // MOVE: s1 is now invalid
println!("{s2}");
println!("{s1}");     // ❌ E0382 borrow of moved value

If both were allowed to own the heap buffer, both would try to free it → double-free. Rust prevents this by transferring ownership and making s1 a compile error.

What the move actually looks like in memory
Before: let s2 = s1;
s1 (String)ptr → ●
heap: "hello"len 5, cap 5
After: ownership handed over
s1 (String)❌ invalidated
s2 (String)ptr → ●
heap: "hello"exactly one owner now

Notice: only the 24-byte stack handle is copied; the heap buffer is not duplicated. A move in Rust is nearly free — it's the ownership bookkeeping that changes, not the data.

Which types are Copy?

Copy (stack-only, cheap to duplicate)Move (owns a resource)
All integers (i32, u64…), floats (f64)
bool, char
Tuples of Copy types: (i32, f64)
Fixed arrays of Copy types: [i32; 4]
References &T (a reference is Copy, but what it points to isn't moved)
Option<CopyType>
String (owns heap text)
Vec<T>, HashMap, Box<T>
File, TcpStream, Mutex (own OS resources)
Any struct containing the above
Any type implementing Drop
A type is Copy only if all its fields are Copy — and you cannot implement Copy on a type that implements Drop.

Getting around a move: clone() vs Copy

clone.rs
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();        // DEEP copy of the heap data — explicit and deliberate
    println!("{s1} {s2}");      // both valid

    // `clone()` is always available on types implementing Clone.
    // It is EXPLICIT by design: Rust wants you to know when you're allocating.
    // Compare with Python/Java where every assignment may secretly copy or retain.
}
⚠️ Avoid "clone() to silence the borrow checker"

When you can't make the borrow checker happy, the tempting fix is to sprinkle .clone() everywhere. It works — and it silently destroys Rust's performance advantage. Treat every clone as a smell. Ask instead: can I pass a reference? can I restructure so the value is returned instead of copied? Chapter 13 (Rc, Arc, Cow) gives you better tools.

Ownership and functions

Passing a value to a function works exactly like assigning it to a variable: moves it in. Returning a value moves it back out.

fn_ownership.rs
fn takes_ownership(s: String) {      // s comes into scope: it now owns the String
    println!("got: {s}");
}                                    // s is dropped here → memory freed ✅

fn makes_copy(x: i32) {              // i32 is Copy, so nothing is moved
    println!("got: {x}");
}

fn gives_ownership() -> String {     // creates a String and hands it to the caller
    String::from("mine now")
}

fn takes_and_gives_back(s: String) -> String {   // borrows it, then gives it back
    println!("borrowed: {s}");
    s
}

fn main() {
    let s = String::from("hello");
    takes_ownership(s);              // s is MOVED into the function
    // println!("{s}");              // ❌ E0382: s was moved

    let x = 5;
    makes_copy(x);                   // fine — x is Copy
    println!("{x}");                 // still fine ✅

    let s1 = gives_ownership();      // s1 owns the returned String
    let s2 = takes_and_gives_back(s1); // s1 moved in, result moved into s2
    // println!("{s1}");             // ❌ moved
    println!("{s2}");                // ✅
}
🧭 The mental model

Think of a String like a house key: there is exactly one. You can hand it to someone else, but then you no longer have it. You can make a copy of the key (clone), or you can let someone borrow the house temporarily (a reference) — but you can't both give away the key and keep using the house.

Borrowing: references that don't take ownership

Instead of moving a value into a function, you can lend it. A reference &T is a pointer you promise not to abuse. The value is not dropped when the reference goes out of scope, because the reference never owned it.

borrowing.rs
fn calculate_length(s: &String) -> usize {   // s is a reference: it borrows
    s.len()                                   // s points to a String it does NOT own
}                                             // s goes out of scope… but nothing is dropped

fn append_greeting(s: &mut String) {          // mutable borrow: may modify
    s.push_str(", world!");
}

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);   // pass a reference — s1 is only borrowed
    println!("'{s1}' is {len} chars"); // ✅ s1 is still valid! No move happened

    let mut s2 = String::from("hello");
    append_greeting(&mut s2);          // pass a MUTABLE reference
    println!("{s2}");                  // hello, world!

    // Reference syntax recap:
    //   &T     → shared/immutable borrow ("read-only lens")
    //   &mut T → exclusive/mutable borrow ("read-write lens")
    //   *&T    → dereference: access the value a reference points to
    let n = 42;
    let r = &n;
    println!("{}", *r + 1);            // 43 — explicit deref
    println!("{}", r + 1);             // 43 — Rust auto-derefs in most contexts
}

The borrowing rules — memorise these

📜

Rule A — the invariant rule

In any given scope you may have either exactly one &mut T reference, or any number of &T references. Never both simultaneously.

📜

Rule B — validity rule

Every reference must always point to valid data. A reference may never outlive the value it points to (no dangling pointers — ever).

Why such a strange rule? Because this is exactly the definition of a data race: two threads reading and writing the same memory with no synchronisation. By making it impossible to hold a mutable and a shared reference at the same time, Rust eliminates data races structurally, at compile time, without any runtime cost.

✅ Valid: many readers
let s = String::from("hi");
let r1 = &s;
let r2 = &s;   // fine: 2 shared borrows
println!("{r1} {r2}");
❌ Invalid: reader + writer
let mut s = String::from("hi");
let r1 = &s;
let r2 = &mut s;  // ❌ E0502
println!("{r1}");
✅ Valid: sequential borrows
let mut s = String::from("hi");
let r1 = &mut s;
r1.push('!');
let r2 = &mut s;   // ok: r1 is dead
println!("{r2}");
❌ Invalid: two writers
let mut s = String::from("hi");
let r1 = &mut s;
let r2 = &mut s;  // ❌ E0499
println!("{r1} {r2}");
💡 Non-lexical lifetimes (NLL) — modern Rust is smarter than you think

Earlier versions of Rust ended a borrow at the closing brace of the scope. Since Rust 2018, a borrow ends at its last use. That's why the third box above compiles: r1 is never used again, so the borrow is already over when r2 is created. If your code looks like it should work, check whether you're accidentally using a variable after its borrow ends.

Slices: borrows with a length

A slice &[T] or &str is a reference plus a length, pointing at a contiguous run of elements. Slices are the idiomatic way to write "give me part of your data" without taking ownership.

slices.rs
// Taking &str instead of &String is more flexible:
// it accepts string literals, &String, and slices alike.
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[..i];     // slice from start up to (not including) i
        }
    }
    &s[..]                       // no space: the whole string
}

fn sum(numbers: &[i32]) -> i32 {  // works with arrays AND Vecs
    numbers.iter().sum()
}

fn main() {
    let sentence = String::from("hello wonderful world");
    let word = first_word(&sentence);
    println!("first word: {word}");      // hello

    let array = [1, 2, 3, 4, 5];
    let vector = vec![10, 20, 30];
    println!("{}", sum(&array));          // 15  — array auto-coerces to slice
    println!("{}", sum(&vector));        // 60  — Vec auto-coerces to slice

    // Slicing syntax: &collection[a..b]  (b exclusive), &collection[a..=b] (inclusive)
    let mid = &array[1..4];              // [2, 3, 4]
    println!("{mid:?}");

    // ⚠️ Slices must land on valid UTF-8 boundaries for str:
    let s = "héllo";
    // let bad = &s[0..2];              // 💥 panic: byte index 2 is not a char boundary
    println!("{:?}", &s[0..1]);          // "h" — safe because é starts at byte 1..3
}
🧩 Why &str over &String in function parameters

This is a real Rust convention you'll see everywhere. &String forces the caller to own a String. &str accepts a literal ("hi"), a String, or any slice of either — it's strictly more general. The same applies to &[T] instead of &Vec<T>. Writing general signatures is a hallmark of idiomatic Rust.

The five most common ownership errors

CodeErrorMeaning & fix
let s2 = s1; use(s1)E0382 use of moved values1 was moved into s2. Fix: clone(), or use s2, or pass &s1.
s = 5 where let sE0384 cannot assign twiceVariables are immutable. Fix: let mut s.
&s and &mut s at onceE0502 cannot borrow as mutableFinish using the shared borrow first (NLL ends it at last use).
two &mut s live at onceE0499 cannot borrow twiceOnly one writer at a time. Restructure into sequential blocks.
let r; { s } r = &s;E0597 does not live long enoughThe reference would dangle. Fix: move the value's declaration outward.
Chapter 06

Lifetimes: keeping references honest

Lifetimes sound scary and are actually simple: they are just labels the compiler uses to prove that no reference will ever point at freed memory. You rarely write them — the compiler usually fills them in for you.

Why do they exist?

Consider a function that returns a reference. Which input does the output borrow from? If the compiler can't tell, it can't prove the returned reference stays valid. A lifetime annotation answers that question:

lifetimes.rs
//                 'a is just a NAME for "the region where x and y are valid"
//                 ┌───┘        ┌───┘
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// Reads as: "given two string slices that live AT LEAST as long as 'a,
//            return a string slice that also lives at least as long as 'a."
// In other words: the returned reference is valid for as long as BOTH inputs are.

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("longest inside the block: {result}");   // ✅ fine
    }
    // println!("{result}");   // ❌ E0597: `s2` does not live long enough
                              // because 'a had to cover both s1 and s2
}
Lifetimes are just spans on a timeline
fn main() openst₀
│ s1 declared  —— lives until end of main  ('a could be here)t₁ ──────── ✅
│ │ s2 declared  —— lives until inner block endst₂ ── ✅
│ │ longest(&s1, &s2) → must return a ref valid for BOTHt₃
│ │ (use result here) ✅ — both still alivet₄
│ └ s2 droppedt₅ 💀
│ (use result here) ❌ — result may point at s2, which is deadt₆

The whole point: the annotation &'a str doesn't change how your code runs at all. It exists purely so the compiler can prove safety. In release builds there is zero overhead — lifetimes are erased entirely after checking.

When you must write a lifetime

  • A function that returns a reference and takes more than one reference parameter.
  • A struct that stores references (then the struct itself needs a lifetime parameter).
  • Whenever the compiler's help message says expected named lifetime parameter.
struct_lifetimes.rs
// A struct that borrows data must declare how long that borrow lasts.
// The instance of Excerpt can never outlive the data it points to.
struct Excerpt<'a> {
    part: &'a str,          // a borrowed string slice
    highlight: usize,
}

impl<'a> Excerpt<'a> {
    // Lifetime elision: one &self input → the output lifetime is assumed to be self's
    fn announce(&self) -> &str {
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first = novel.split('.').next().expect("no sentence");
    let e = Excerpt { part: first, highlight: 7 };
    println!("{}", e.announce());
}   // e drops, then novel drops — order is always reverse of declaration

Lifetime elision rules — why you usually write none

The compiler applies three automatic rules. If they disambiguate every reference, you don't need annotations:

Rule 1Each &param gets its own fresh lifetime: fn f(x: &str, y: &str) becomes fn f<'a,'b>(x: &'a str, y: &'b str).
→
Rule 2If there is exactly one input lifetime, it's assigned to all output lifetimes: fn f(x: &str) -> &str works with no annotations.
→
Rule 3If one of the inputs is &self or &mut self, the output borrows from self.
→
OtherwiseThe compiler genuinely can't infer → you must annotate, and it will tell you exactly where.

The 'static lifetime

static_life.rs
// 'static means "lives for the entire duration of the program".
// String literals are 'static: they're baked into the compiled binary.
fn returns_static() -> &'static str {
    "I live forever"
}

fn main() {
    let s: &'static str = returns_static();
    println!("{s}");

    // ⚠️ Common beginner instinct: "I'll just add 'static to make it compile."
    // That is almost always wrong. 'static is not a fix — it's a promise the
    // compiler will demand you keep. Fix the ownership structure instead.
}
💡 Practical advice

For your first months of Rust, you will write lifetimes almost never. The compiler's elision rules cover the vast majority of code, and when it doesn't, the error message hands you the exact annotation to add. Focus on ownership and borrowing first; lifetimes will feel like punctuation rather than a new concept.

Chapter 07

Structs, enums & pattern matching

Rust's type system lets you model your data so precisely that illegal states become unrepresentable. If a function only accepts a validated email, a validated email type makes it impossible to pass anything else.

Structs — named groups of data

structs.rs
// Deriving traits gives you useful behaviour for free:
// Debug → println!("{:?}"), Clone → .clone(), PartialEq → ==
#[derive(Debug, Clone, PartialEq)]
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

// Tuple struct: useful for lightweight wrappers / newtypes
struct Color(u8, u8, u8);
struct Meters(f64);

// Unit struct: no data, but has a type identity (great as markers/traits)
struct AlwaysEqual;

fn build_user(email: String, username: String) -> User {
    User {
        email,               // field init shorthand: same name as the variable
        username,
        sign_in_count: 1,
        active: true,
    }
}

fn main() {
    let mut user = build_user(String::from("a@b.com"), String::from("ada"));
    user.email = String::from("new@b.com");     // mutate: needs `mut`

    // Struct update syntax: fill remaining fields from another instance
    let user2 = User { email: String::from("x@y.com"), ..user.clone() };

    println!("{user:?}");    // pretty-print with :?  (or {:#?} for multi-line)
    println!("{}", user2.username);

    let black = Color(0, 0, 0);
    println!("({}, {}, {})", black.0, black.1, black.2);

    let d = Meters(10.0);
    println!("{}m", d.0);
}

Methods & associated functions with impl

methods.rs
#[derive(Debug)]
struct Rectangle { width: f64, height: f64 }

impl Rectangle {
    // ── associated function ("static method") — called as Rectangle::square(3.0)
    fn square(size: f64) -> Self {          // Self = Rectangle
        Self { width: size, height: size }
    }

    // ── methods take `self` in one of three flavours:
    fn area(&self) -> f64 {                 // borrow: read-only, most common
        self.width * self.height
    }

    fn scale(&mut self, factor: f64) {      // mutable borrow: modify in place
        self.width *= factor;
        self.height *= factor;
    }

    fn into_parts(self) -> (f64, f64) {     // take ownership: consume the value
        (self.width, self.height)
    }

    // ── multiple impl blocks are allowed (used for traits and conditional impls)
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect = Rectangle::square(5.0);
    println!("area = {}", rect.area());          // 25
    rect.scale(2.0);
    println!("scaled = {}", rect.area());        // 100
    println!("{:?}", rect);                      // Rectangle { width: 10.0, height: 10.0 }
    let (w, h) = rect.into_parts();              // rect is consumed here
    println!("{w} x {h}");
}

Enums — every variant, and the data it carries

This is one of Rust's most powerful features. Enum variants can hold any data, including different types per variant, tuples, structs, or even other enums. This replaces class hierarchies, sentinel values and null entirely.

enums.rs
#[derive(Debug)]
enum Shape {
    Circle(f64),                            // variant with one field: radius
    Rectangle { width: f64, height: f64 },  // variant with named fields
    Triangle(f64, f64, f64),                // variant with three fields
    Nothing,                                // unit variant
}

// The most important enum in the entire language —
// it replaces null with a type you are FORCED to check.
enum Option<T> {
    None,        // no value
    Some(T),     // a value of type T
}

#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

// Enums can have methods too
impl Message {
    fn summary(&self) -> String {
        match self {
            Message::Quit => String::from("quit"),
            Message::Move { x, y } => format!("move to {x},{y}"),
            Message::Write(text) => format!("write: {text}"),
            Message::ChangeColor(r, g, b) => format!("colour {r},{g},{b}"),
        }
    }
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()   // Heron's formula
        }
        Shape::Nothing => 0.0,
    }
}

fn main() {
    let shapes = [
        Shape::Circle(2.0),
        Shape::Rectangle { width: 3.0, height: 4.0 },
        Shape::Triangle(3.0, 4.0, 5.0),
        Shape::Nothing,
    ];
    for s in &shapes {
        println!("{s:?} has area {:.2}", area(s));
    }

    let m = Message::Write(String::from("hi"));
    println!("{}", m.summary());
}

Pattern matching with match

match is like a switch on steroids: it destructures data, must be exhaustive (cover every case), and each arm is an expression.

matching.rs
fn main() {
    // ── literals, ranges, and catch-alls ────────────────────────────────
    let n = 13;
    match n {
        1 => println!("one"),
        2 | 3 | 5 | 7 | 11 => println!("small prime"),   // multiple patterns with |
        12..=19 => println!("teenager"),                  // inclusive range
        _ => println!("something else"),                  // catch-all (required!)
    }

    // ── binding values with @ and destructuring ─────────────────────────
    let point = (3, -7);
    match point {
        (0, 0) => println!("origin"),
        (x, 0) => println!("on the x axis at {x}"),
        (0, y) => println!("on the y axis at {y}"),
        (x, y) if x == y => println!("diagonal"),         // match GUARD
        (x, y) => println!("point ({x}, {y})"),
    }

    // ── destructuring enums, structs and references ─────────────────────
    #[derive(Debug)]
    enum Coin { Penny, Nickel, Dime, Quarter(String) }

    fn value(c: &Coin) -> u8 {
        match c {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter(state) => { println!("quarter from {state}"); 25 }
        }
    }
    println!("{}", value(&Coin::Quarter(String::from("Texas"))));

    // ── nested patterns with .. ─────────────────────────────────────────
    let triple = (1, 2, 3, 4, 5);
    match triple {
        (first, .., last) => println!("first={first}, last={last}"),
    }
}

Concise alternatives to full match

shorthand.rs
fn main() {
    let config_max: Option<u8> = Some(3);

    // if let: when you only care about ONE case
    if let Some(max) = config_max {
        println!("max is {max}");
    } else {
        println!("no maximum");
    }

    // while let: loop while a pattern keeps matching
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        print!("{top} ");       // 3 2 1
    }
    println!();

    // let-else: handle the failure case and continue (Rust 1.65+)
    fn get_id() -> Option<u32> { Some(42) }
    let Some(id) = get_id() else {
        println!("no id available");
        return;
    };
    println!("id = {id}");

    // matches!: a bool macro — great inside .filter() or assertions
    let x = Some(3);
    println!("{}", matches!(x, Some(3)));            // true
    let nums = [1, 2, 3, 4, 5];
    let evens: Vec<_> = nums.iter().filter(|&&n| matches!(n, x if x % 2 == 0)).collect();
    println!("{evens:?}");
}
💡 Exhaustiveness is a superpower

When you add a new enum variant, the compiler immediately lists every match in your codebase that must now handle it. In a language with booleans or null, that same change silently breaks code you forgot about. Teams switching to Rust routinely report that this single feature eliminates a whole class of production incidents.

Option<T> versus null

Java / C# / JS / Python nullRust Option<T>
Is absence explicit?No — any variable might be null at any timeYes — Option<T> is a different type
What happens if you forget?NullPointerException at runtime, possibly in production, possibly at 3amCompile error — you cannot get a T out of an Option<T> without a match
Runtime costHidden null checks everywhere in generated codeZero — niche optimisation makes Option<&T> the same size as &T
CompositionManual null checks, often forgotten.map(), .and_then(), .unwrap_or(), ?
DocumentationNone — read the source to see if null is possibleThe type signature is the documentation
Chapter 08

Collections & strings

The standard library's collections live in std::collections: Vec, HashMap, HashSet, BTreeMap, VecDeque and more. And then there's the String question — which deserves its own section.

Vec<T> — the growable list

vec.rs
fn main() {
    // Creating
    let mut v: Vec<i32> = Vec::new();       // empty, type inferred from use
    let mut v2 = vec![1, 2, 3];             // macro with initial values
    let zeros = vec![0u8; 1024];            // 1024 zero bytes

    // Adding & removing
    v.push(10);
    v.push(20);
    v.push(30);
    let last = v.pop();                     // Option<i32> — Some(30) or None if empty
    println!("{last:?}");

    // Reading — two ways:
    let third = &v2[2];                     // panics if out of bounds (fast path)
    let maybe = v2.get(99);                 // returns Option<&i32> — never panics
    match maybe {
        Some(val) => println!("found {val}"),
        None      => println!("no element there"),
    }

    // Iterating — note: &v (borrow), v.iter(), or v.into_iter() (consume)
    for x in &v2 { print!("{x} "); }        // 1 2 3
    println!();
    for x in &mut v2 { *x *= 10; }          // mutate in place
    println!("{v2:?}");                     // [10, 20, 30]

    // Storing different types: use an enum or Box<dyn Trait>
    #[derive(Debug)]
    enum Cell { Int(i32), Text(String) }
    let row = vec![Cell::Int(1), Cell::Text(String::from("hello"))];
    println!("{row:?}");

    // Common operations
    let mut nums = vec![5, 2, 8, 1, 9];
    nums.sort();                             // [1, 2, 5, 8, 9]
    nums.dedup();                            // removes consecutive duplicates
    nums.retain(|&n| n > 2);               // keep only matching elements
    println!("{nums:?}");                    // [5, 8, 9]
    println!("{}", nums.iter().sum::<i32>()); // 22
    let doubled: Vec<i32> = nums.iter().map(|n| n * 2).collect();
    println!("{doubled:?}");                 // [10, 16, 18]
}
⚠️ Holding a borrow while mutating the collection

This is a classic borrow-checker wall:

let mut v = vec![1, 2, 3];
let first = &v[0];       // shared borrow begins
v.push(4);               // ❌ E0502: cannot borrow `v` as mutable
println!("{first}");     // …because the borrow is still used here

The compiler is right: push may reallocate, invalidating first. Fix it by copying the value out (let first = v[0];) or ending the borrow before mutating. This bug is a silent heap corruption in C++.

HashMap<K, V> — key/value lookups

hashmap.rs
use std::collections::{HashMap, HashSet, BTreeMap};

fn main() {
    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // Insert-if-absent / update-if-present — the `entry` API prevents double lookups
    scores.entry(String::from("Blue")).or_insert(25);      // stays 10 (already there)
    scores.entry(String::from("Red")).or_insert(99);       // inserted as 99

    // Count words — the classic `entry` idiom
    let text = "hello world wonderful hello";
    let mut counts: HashMap<&str, usize> = HashMap::new();
    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }
    println!("{counts:?}");

    // Iterating yields (&K, &V)
    for (team, score) in &scores {
        println!("{team}: {score}");
    }

    // Lookup returns Option<&V> — never a null, never a panic
    let blue = scores.get("Blue");        // Option<&i32>
    println!("{blue:?}");

    // Ownership rules apply! These would move the strings out of the map:
    // scores.insert(String::from("x"), 1); let k = String::from("x"); scores.get(&k);

    // BTreeMap: sorted keys, O(log n) — use when you need ordered iteration
    let ordered: BTreeMap<&str, i32> = [("b", 2), ("a", 1), ("c", 3)].into();
    println!("{ordered:?}");

    // HashSet: uniqueness with O(1) membership checks
    let mut seen: HashSet<String> = HashSet::new();
    seen.insert(String::from("alice"));
    println!("{}", seen.contains("alice"));   // true
}

Choosing a collection

TypeOrderingLookupInsertUse when…
Vec<T>InsertionO(1) by indexAmortised O(1)Default choice for a list. Cache-friendly, fastest iteration.
VecDeque<T>InsertionO(1)O(1) both endsQueues, FIFO buffers, sliding windows.
HashMap<K,V>None (random)O(1) avgO(1) avgFastest lookups; iteration order varies per run.
BTreeMap<K,V>Sorted by keyO(log n)O(log n)Need sorted keys, ranges, or deterministic output.
HashSet<T>NoneO(1) avgO(1) avgUniqueness, membership tests.
LinkedList<T>InsertionO(n)O(1) endsAlmost never — poor cache locality; prefer Vec or VecDeque.
Rust's standard library deliberately keeps this list small. The indexmap, dashmap and smallvec crates cover specialised needs.

String vs &str — the complete picture

Rust's string handling is famously strict, and the reason is Unicode correctness. Here is every fact you need:

📦

String

Owned, heap-allocated, growable, UTF-8. Stored as three words: pointer, length, capacity. Created with String::new(), String::from("hi") or "hi".to_string().

🔍

&str

A borrowed view into UTF-8 bytes: pointer + length, no allocation. A string literal "hi" is a &'static str. Use it in function parameters.

strings.rs
fn main() {
    // Creating & converting
    let s1 = String::from("hello");
    let s2 = s1.clone();
    let s3 = "literal".to_string();
    let s4 = format!("{}-{}", "a", "b");     // like println! but returns a String
    let borrowed: &str = &s1;                // borrow a String as a slice
    let owned: String = borrowed.to_owned(); // back to an owned String

    // Building
    let mut s = String::from("hello");
    s.push(' ');                             // single char
    s.push_str("world");                     // string slice
    s += "!";                                // via AddAssign
    let combined = format!("{s} again");     // format! never moves its inputs
    println!("{combined}");

    // ⚠️ Slicing by BYTES — this trips everyone up
    let s = "Здравствуйте";                  // Cyrillic: 2 bytes per character
    println!("chars: {}", s.chars().count());        // 12
    println!("bytes: {}", s.len());                  // 24
    // let slice = &s[0..2];                         // 💥 panics: not a char boundary

    // Iterate safely
    for c in "नमस्ते".chars() { print!("{c}"); }       // by Unicode scalar value
    println!();
    for b in "abc".bytes() { print!("{b} "); }       // 97 98 99
    println!();

    // Searching & transforming
    let sentence = "the quick brown fox jumps over the lazy dog";
    println!("{}", sentence.contains("brown"));           // true
    println!("{}", sentence.starts_with("the"));          // true
    println!("{}", sentence.replace("dog", "cat"));
    let words: Vec<&str> = sentence.split_whitespace().collect();
    println!("{} words", words.len());                    // 9
    println!("{}", sentence.to_uppercase());
    println!("{:?}", sentence.split(',').collect::<Vec<_>>());
}

Why strings can't be indexed like arrays

One character is not always one byte
String "hi" in ASCII
[0] 'h'1 byte
[1] 'i'1 byte
s[0..2] == "hi"✅ predictable
String "naïve" with an accent
'n' 'a'1 + 1 bytes
'ï' (U+00EF)2 bytes: 0xC3 0xAF
'v' 'e'1 + 1 bytes
s[0..3] would split ï in half💥 panic

Rust forbids s[i] on String because a byte index may fall in the middle of a character, producing invalid UTF-8 — which is a safety invariant of the type. Use s.chars(), s.bytes(), or a slice aligned to char boundaries.

Formatting cheat sheet

PlaceholderMeaningExampleOutput
{}Display (human-readable)format!("{}", 42)42
{:?}Debug (developer output)format!("{:?}", vec![1,2])[1, 2]
{:#?}Debug, pretty multi-lineformat!("{:#?}", p)indented tree
{name}Named argumentformat!("{x} of {y}", x=1, y=2)1 of 2
{0} {1} {0}Positional argumentsformat!("{0}-{1}-{0}", "a", "b")a-b-a
{:.2}Precision (2 decimals)format!("{:.2}", 3.14159)3.14
{:05}Zero-pad to width 5format!("{:05}", 42)00042
{:10}Pad to width 10format!("{:10}|", "hi")hi |
{:<10} {:^10}Left / centre alignmentformat!("{:^7}", "hi") hi
{:x} {:#x} {:o} {:b}Hex, prefixed hex, octal, binaryformat!("{:#x}", 255)0xff
{:e}Scientific notationformat!("{:e}", 100000.0)1e5
{:.3?}Debug with limited precision (floats in structs)format!("{:.3?}", f)rounded floats
Tip: implement Display ({}) for your own types with impl std::fmt::Display for MyType, and derive Debug ({:?}) with #[derive(Debug)].
Chapter 09

Error handling without exceptions

Rust has no exceptions and no try/catch. Errors are ordinary values you must acknowledge. This feels verbose at first, then becomes a superpower: every failure mode is visible in the function signature.

Two categories of error

💥

Unrecoverable — panic!

For bugs: index out of bounds, integer overflow in debug, invariant violated. The thread unwinds (or aborts), prints a backtrace, and exits. Similar to an exception you should never catch.

🔁

Recoverable — Result<T, E>

For expected failures: file not found, network timeout, invalid user input. Returned as a value, handled by the caller, and impossible to ignore thanks to #[must_use].

panics.rs
fn main() {
    // Explicit panic with a message
    // panic!("crashed and burned");

    // These all panic automatically:
    let v = vec![1, 2, 3];
    // let x = v[99];                       // 💥 index out of bounds
    // let y: u8 = 255; let z = y + 1;      // 💥 overflow (debug builds only)
    // let n: i32 = "abc".parse().unwrap(); // 💥 called `Option::unwrap()` on `Err`

    // A graceful way to stop the program when there's no sensible recovery:
    let config = std::fs::read_to_string("config.toml")
        .expect("config.toml must exist for the app to run");  // `.expect(msg)` = unwrap + message

    println!("{config}");
}
⚠️ Reading a panic message

thread 'main' panicked at src/main.rs:4:13: index out of bounds: the len is 3 but the index is 99 — that gives you the file, the line, the column, and the reason. Run with RUST_BACKTRACE=1 for a full stack trace, or RUST_BACKTRACE=full for maximum detail. In 2024 Rust added panic backtraces in debug builds by default.

Result<T, E> — the workhorse

results.rs
use std::num::ParseIntError;

// The signature TELLS you this can fail. No documentation needed.
fn parse_age(input: &str) -> Result<u8, ParseIntError> {
    input.trim().parse::<u8>()      // `parse` already returns Result
}

fn main() {
    // Pattern matching is the most explicit way to handle it
    match parse_age("42") {
        Ok(age)  => println!("age is {age}"),
        Err(e)   => println!("could not parse: {e}"),
    }

    // if let for one-sided handling
    if let Ok(age) = parse_age("29") {
        println!("got {age}");
    }

    // Combinators: transform without branching
    let doubled = parse_age("21").map(|age| age * 2);        // Result<u8, _>
    println!("{doubled:?}");                                  // Ok(42)

    // Fallbacks
    let age = parse_age("oops").unwrap_or(0);                 // 0 on failure
    let age = parse_age("oops").unwrap_or_else(|_| 18);       // compute a default
    let age = parse_age("oops").unwrap_or_default();          // 0 (Default trait)
    println!("{age}");

    // `?` — the propagating operator (the idiomatic way)
    fn process() -> Result<u8, ParseIntError> {
        let a = parse_age("10")?;      // on Err: return early from `process`
        let b = parse_age("20")?;
        Ok(a + b)
    }
    println!("{:?}", process());       // Ok(30)
}

The ? operator in depth

You writelet x = may_fail()?;
→
Compiler generateslet x = match may_fail() { Ok(v) => v, Err(e) => return Err(From::from(e)), };
→
RequirementsYour function must return Result<…> or Option<…>. The error type must be convertible via From — which is how one ? handles many error kinds.
question_mark.rs
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// A custom error type — see `thiserror` for a derive macro that writes this for you
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    Invalid(String),
}

// Implementing `From` lets `?` convert automatically
impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn read_number_from(path: &str) -> Result<i32, AppError> {
    let mut file = File::open(path)?;             // io::Error → AppError via From
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;          // same
    let n = contents.trim().parse::<i32>()?;      // ParseIntError → AppError
    if n < 0 {
        return Err(AppError::Invalid(String::from("must be positive")));
    }
    Ok(n)
}

fn main() {
    match read_number_from("number.txt") {
        Ok(n)  => println!("read {n}"),
        Err(e) => println!("failed: {e:?}"),
    }
}
💡 The fast path for applications: Box<dyn Error>

For quick scripts and binary entry points, you don't need a custom error type:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let content = std::fs::read_to_string("data.txt")?;   // works for ANY error type
    let n: i32 = content.trim().parse()?;
    println!("{n}");
    Ok(())
}

For libraries, prefer the thiserror crate (typed errors) and let anyhow handle ad-hoc errors at the application layer.

Option<T> toolbox

option_tools.rs
fn main() {
    let x: Option<i32> = Some(5);
    let e: Option<i32> = None;

    // Transform
    println!("{:?}", x.map(|n| n * 2));             // Some(10)
    println!("{:?}", e.map(|n| n * 2));             // None

    // Chain operations that themselves return Option
    println!("{:?}", x.and_then(|n| Some(n + 1)));  // Some(6)

    // Provide defaults
    println!("{}", e.unwrap_or(0));                 // 0
    println!("{}", e.unwrap_or_else(|| expensive()));  // computed only when None
    println!("{}", e.unwrap_or_default());          // 0

    // Combine two Options
    let a = Some(1); let b = Some(2);
    println!("{:?}", a.zip(b));                     // Some((1, 2))
    println!("{:?}", a.and(b));                     // Some(2)  (both must exist)
    println!("{:?}", a.or(e));                      // Some(1)  (first one that exists)

    // Convert
    println!("{:?}", x.ok_or("missing"));           // Result<i32, &str> = Ok(5)
    let r: Result<i32, String> = Ok(3);
    println!("{:?}", r.ok());                       // Some(3)

    // Side effects without unwrapping
    if let Some(n) = x { println!("have {n}"); }
    x.inspect(|n| println!("found {n}"));
    println!("{}", x.is_some_and(|n| n > 3));       // true
}

fn expensive() -> i32 { 42 }
SituationDo thisNot this
You can genuinely handle itmatch / if let / unwrap_or.unwrap()
It's a bug if it's absent.expect("reason why this is always Some").unwrap() with no explanation
Caller should decideReturn Result<T, E>Return T and panic inside
Only the presence matters.is_some() / .is_none()Comparing to null
Library codeAlways Result — never panic on user inputPanic on expected failures
Rule of thumb: .unwrap() in production code is a bug report waiting to happen. .expect() with a clear message is acceptable when the invariant is genuinely guaranteed.
Chapter 10

Generics & traits

Generics let one piece of code work for many types. Traits define shared behaviour — Rust's equivalent of interfaces, but more powerful and with zero runtime overhead.

Generics — write once, use for any type

generics.rs
// Without generics you'd write this twice (or three times, or ten times):
fn largest_i32(list: &[i32]) -> i32 { let mut m = list[0]; for &n in list { if n > m { m = n; } } m }
fn largest_f64(list: &[f64]) -> f64 { let mut m = list[0]; for &n in list { if n > m { m = n; } } m }

// With generics: one implementation
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut biggest = &list[0];
    for item in list {
        if item > biggest { biggest = item; }
    }
    biggest
}

// Generic structs
struct Point<T> { x: T, y: T }
struct Pair<A, B> { first: A, second: B }

// Generic methods — the <T> after `impl` declares the type parameter
impl<T> Point<T> {
    fn x(&self) -> &T { &self.x }
}

// Specialised: only for Point<f64>
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let nums = vec![34, 50, 25, 100, 65];
    println!("largest = {}", largest(&nums));       // 100

    let ints = Point { x: 5, y: 10 };
    let floats = Point { x: 1.5, y: 4.2 };
    println!("{} {}", ints.x(), floats.distance_from_origin());

    let pair = Pair { first: 1, second: "one" };
    println!("{:?} {:?}", pair.first, pair.second);
}

Monomorphisation: why generics are free

What happens at compile time
Your sourcefn max<T: Ord>(a: T, b: T) -> T { ... } max(1i32, 2i32); max("a", "b");
→
Compiler generatesfn max_i32(a: i32, b: i32) -> i32 { ... } fn max_str(a: &str, b: &str) -> &str { ... }
→
ResultEach call site gets a specialised function with real types, so the CPU executes plain, fully-optimised native code. No boxing, no vtables, no lookups. This is the difference from Java's erased generics or C#'s boxed generics.

Trade-off: monomorphisation increases compile time and binary size (one copy per type). Rust accepts this because runtime speed is the priority. Dynamic dispatch (dyn Trait) is the opt-out when binary size matters.

Traits — shared behaviour

traits.rs
// Define a trait: a named set of methods
trait Summary {
    // Required method
    fn summarize(&self) -> String;

    // Default method — implementors may override it
    fn headline(&self) -> String {
        format!("Read more… ({})", self.summarize())
    }
}

struct Article { title: String, author: String, content: String }
struct Tweet { username: String, body: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author)
    }
    // headline() uses the default
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.body)
    }
    fn headline(&self) -> String {
        format!("New tweet from @{}", self.username)   // override the default
    }
}

// ── Trait bounds: "any type that implements Summary" ─────────────────────
fn notify(item: &impl Summary) {                 // clean syntax
    println!("Breaking: {}", item.summarize());
}

fn notify_verbose<T: Summary>(item: &T) {        // identical meaning
    println!("Breaking: {}", item.summarize());
}

// Multiple bounds with `+`
fn both(item: &(impl Summary + std::fmt::Display)) {}

// Where clauses make complex bounds readable
fn complex<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: Summary + std::fmt::Debug + ?Sized,
{
    format!("{} and {:?}", t.summarize(), u)
}

fn main() {
    let a = Article { title: String::from("Rust 1.80"), author: String::from("Ferris"),
                      content: String::from("…") };
    let t = Tweet { username: String::from("rustlang"), body: String::from("new release!") };
    notify(&a);
    notify(&t);
    println!("{}", t.headline());
}

Static vs dynamic dispatch

impl Trait / T: Trait (static)Box<dyn Trait> / &dyn Trait (dynamic)
ResolvedAt compile time (monomorphised)At runtime, via a vtable pointer
SpeedMaximum — fully inlinableSlight indirection cost (usually negligible)
Binary sizeGrows with each type usedOne copy of the code total
CollectionsOne concrete type per containerVec<Box<dyn Shape>> can mix types
Object safetyNo such requirementTrait must be "object safe" (no generic methods, no Self: Sized)
Use whenDefault choice. Performance-critical codePlugin systems, heterogeneous lists, reducing compile times
dyn_dispatch.rs
use std::fmt::Debug;

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str { "shape" }
}

struct Circle(f64);
struct Rect { w: f64, h: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
    fn name(&self) -> &str { "circle" }
}
impl Shape for Rect {
    fn area(&self) -> f64 { self.w * self.h }
    fn name(&self) -> &str { "rectangle" }
}

fn main() {
    // Static dispatch: generic, fast, but `shapes` must be homogeneous
    fn total_static(items: &[&dyn Shape]) -> f64 {
        items.iter().map(|s| s.area()).sum()
    }

    // Dynamic dispatch: one Vec can hold DIFFERENT concrete types
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle(1.0)),
        Box::new(Rect { w: 2.0, h: 3.0 }),
    ];
    for s in &shapes {
        println!("{}: {:.2}", s.name(), s.area());
    }
    println!("total: {:.2}", total_static(&[&Circle(1.0), &Rect{w:2.0,h:3.0}]));

    // impl Trait in return position: "some type that implements Display"
    fn make_greeting() -> impl std::fmt::Display {
        String::from("hello")
    }
    println!("{}", make_greeting());
}

Essential standard-library traits

TraitGives youDerive?
Debug{:?} formatting — essential for debugging#[derive(Debug)]
Display{} formatting for humans — not derivable, implement by hand❌
Clone.clone() — explicit deep copy#[derive(Clone)]
CopyImplicit bitwise copy on assignment (requires Clone)#[derive(Copy, Clone)]
PartialEq / Eq== and !=✅
PartialOrd / Ord<, >, .sort(), BTreeMap keys✅
HashUsable as a HashMap/HashSet key✅
DefaultT::default() — a sensible empty value✅
From / IntoConversions: T::from(x), x.into()❌
TryFrom / TryIntoFallible conversions returning Result❌
Iterator.next(), and 60+ combinators for free❌
Deref / DerefMutSmart-pointer behaviour, method auto-deref❌
DropCustom cleanup at end of scope (RAII)❌
Fn / FnMut / FnOnceClosure calling conventions❌
Send / SyncThread-safety markers (auto-derived)auto
AsRef<T> / AsMut<T>Cheap borrow conversions (e.g. String → &str)❌
Convention: implement From and you get Into for free. Implement PartialEq + Eq + Hash together consistently or HashMap will misbehave.
implement_display.rs
use std::fmt;

#[derive(Debug, PartialEq)]
struct Color { r: u8, g: u8, b: u8 }

// Display must be implemented by hand — Rust wants your human output to be deliberate
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
    }
}

// From<&str> gives us Into<Color> automatically
impl From<&str> for Color {
    fn from(hex: &str) -> Self {
        let n = u32::from_str_radix(hex.trim_start_matches('#'), 16).unwrap_or(0);
        Color { r: ((n >> 16) & 0xFF) as u8, g: ((n >> 8) & 0xFF) as u8, b: (n & 0xFF) as u8 }
    }
}

fn main() {
    let c = Color { r: 249, g: 115, b: 22 };
    println!("Display: {c}");          // #f97316
    println!("Debug:   {c:?}");        // Color { r: 249, g: 115, b: 22 }
    let c2: Color = "#22c55e".into();  // uses From<&str>
    println!("{c2}");                  // #22c55e
    println!("{}", c == c2);           // false (PartialEq)
}

The newtype pattern & extension traits

newtype.rs
// NEWTYPE: a tuple struct wrapping a primitive gives you type safety at ZERO cost.
struct Meters(f64);
struct Seconds(f64);

fn speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}

// Now `speed(Seconds(10.0), Meters(100.0))` is a COMPILE ERROR.
// You cannot accidentally mix units — a huge win for numerical code.

// EXTENSION TRAIT: add methods to types you don't own
trait StrExt {
    fn shout(&self) -> String;
}

impl StrExt for str {
    fn shout(&self) -> String {
        format!("{}!!!", self.to_uppercase())
    }
}

// ASSOCIATED TYPES: cleaner when a trait has exactly one sensible type per implementor
trait Container {
    type Item;                       // one type, chosen by the implementor
    fn insert(&mut self, item: Self::Item);
    fn first(&self) -> Option<&Self::Item>;
}

fn main() {
    let d = Meters(100.0);
    let t = Seconds(9.58);
    println!("speed = {:.2} m/s", speed(d, t));
    println!("{}", "hello".shout());
}
Chapter 11

Modules, crates & packages

Rust's module system is explicit and privacy-first: everything is private unless you mark it pub. Combined with cargo, this gives you one of the cleanest dependency stories in any language.

The vocabulary

📦

Package

A Cargo.toml describing one or more crates. What you publish.

🔧

Crate

A compilation unit. Either a binary (has main.rs) or a library (has lib.rs).

🗂️

Module

A namespace inside a crate. Controls scoping and privacy.

🔗

Path

How you name an item: crate::a::b::Item (absolute) or self::x, super::x.

src/lib.rs (module tree)
// Declaring modules: the compiler looks for either a `garden` directory
// or a `garden.rs` file next to this one.
pub mod garden;              // src/garden.rs  (or src/garden/mod.rs)

// Inline module — good for small ones
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn seat_at_table() {}        // private: invisible outside `hosting`
    }
}

use crate::front_of_house::hosting;   // import: now you can write `hosting::…`
use self::garden::Vegetables;         // explicit self::
use std::collections::HashMap;        // external crate
use std::fmt::Result as FmtResult;    // rename to avoid collisions
use std::io::{self, Write};           // import `io` AND `io::Write`

// Re-export: make an internal type part of your public API
pub use garden::Vegetables;

fn eat_at_restaurant() {
    hosting::add_to_waitlist();       // via the `use`
    crate::front_of_house::hosting::add_to_waitlist();   // absolute path
}

// Privacy rules:
// • Items are PRIVATE by default, including struct fields.
// • A child module may access its ancestors' private items.
// • A parent may NOT access a child's private items.
// • `pub(crate)` = visible within the current crate only (very useful).

Real-world file layout

project structure
src/
├── lib.rs              # crate root: `pub mod gui;` `pub mod models;`
├── main.rs             # thin binary that calls into the library crate
├── gui/
│   ├── mod.rs          # declares `pub mod buttons;` `pub mod panels;`
│   ├── buttons.rs
│   └── panels.rs
├── models/
│   ├── mod.rs
│   ├── user.rs
│   └── order.rs
└── utils.rs

# NOTE: in Rust 2018+ you may use `gui.rs` + a `gui/` folder instead of `mod.rs`
# (both work; `foo.rs` + `foo/bar.rs` is now the recommended style)

Dependencies & crates.io

terminal + Cargo.toml
cargo add serde --features derive    # adds a dependency with a feature flag
cargo add tokio --features full
cargo search regex                   # search crates.io from the terminal
cargo tree --duplicates              # find duplicated dependencies
cargo update -p serde                # update one crate only
Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json"] }
chrono = "0.4"                       # caret requirement: ^0.4 (semver compatible)
uuid = { version = "1", features = ["v4"] }

[dependencies.my_internal_lib]
path = "../my_internal_lib"          # local path dependency

[features]
default = ["json"]
json = ["serde"]
cli = ["clap"]

[workspace]
members = ["core", "cli", "web"]     # multi-crate workspace: one Cargo.lock, shared build
Version specMeaningAllows updates to
"1.2.3"Caret: ^1.2.3 (the default)1.2.3 → 1.9.9, not 2.0
"~1.2.3"Tilde: patch updates only1.2.3 → 1.2.9
"=1.2.3"Exact pinNothing
"1.2"Equivalent to ^1.21.2.0 → 1.9.9
"*"Any version (avoid!)Everything
Commit Cargo.lock for binaries and applications (reproducible builds). Do not commit it for libraries — let the consumer resolve versions.
💡 Evaluating a crate before you depend on it
  • Check downloads & recent release date on crates.io
  • Read the docs on docs.rs — quality varies hugely
  • Look for "reverse dependencies" (how many crates rely on it)
  • Run cargo deny or cargo audit for licence and vulnerability checks
  • Prefer small, focused crates over heavyweight frameworks when learning
Chapter 12

Closures & iterators

This is where Rust code starts to feel genuinely expressive. Closures capture their environment; iterators chain transformations lazily — and both compile down to the same fast code as a hand-written loop.

Closures — anonymous functions

closures.rs
fn main() {
    // Syntax: |params| body    (Type annotations optional — inferred from use)
    let add = |a: i32, b: i32| a + b;
    let square = |x: i32| { x * x };        // braces when you need multiple statements
    let greeting = || println!("hi");       // no params

    println!("{} {}", add(2, 3), square(4));

    // Closures capture variables from their enclosing scope
    let offset = 10;
    let add_offset = |x: i32| x + offset;   // borrows `offset`
    println!("{}", add_offset(5));          // 15

    // move closures TAKE ownership of captured variables
    let name = String::from("Ferris");
    let greet = move || println!("hello, {name}");  // `name` moved into the closure
    greet();
    // println!("{name}");   // ❌ E0382: name was moved into the closure

    // This is essential for threads and async: the closure must outlive the scope
    let data = vec![1, 2, 3];
    let handle = std::thread::spawn(move || {
        println!("thread sees {data:?}");   // owns `data` now
    });
    handle.join().unwrap();
}

The three closure traits

Fn family — decided by HOW the closure uses its captures
Fnfn(&self)Reads captured variables only. Can be called many times. Most common.
→
FnMutfn(&mut self)Modifies captures (e.g. a counter). Callable many times.
→
FnOncefn(self)Consumes a capture (moves out of it). Callable only once.
→
RuleEvery Fn is also FnMut, and every FnMut is also FnOnce. The compiler picks the most restrictive that fits your closure's body.
closure_traits.rs
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }

fn call_twice<F: FnMut()>(mut f: F) { f(); f(); }

fn consume_once<F: FnOnce() -> String>(f: F) -> String { f() }

fn main() {
    println!("{}", apply(|x| x * 2, 21));         // 42

    let mut count = 0;
    call_twice(|| { count += 1; });                // FnMut closure
    println!("count = {count}");                   // 2

    let name = String::from("Ferris");
    let result = consume_once(move || name);       // FnOnce: consumes `name`
    println!("{result}");
}

Iterators — lazy, composable, fast

An iterator is any type implementing Iterator, which requires just one method:

iterator_trait.rs
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // …50+ provided methods come free
}
🧭 Laziness: the key mental model

Adapters like .map() and .filter() do nothing when called. They build a description of a computation. Only a consumer — .collect(), .sum(), .for_each(), or a for loop — actually pulls values through the pipeline, one at a time. This means you can compose an infinite iterator and still terminate.

iterators.rs
fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // ── adapters (lazy) ────────────────────────────────────────────────
    let evens_doubled: Vec<i32> = numbers.iter()
        .filter(|&&n| n % 2 == 0)      // &&n because .iter() yields &i32 and filter passes &&i32
        .map(|n| n * 2)
        .collect();                     // ← consumer: this is when it runs
    println!("{evens_doubled:?}");      // [4, 8, 12, 16, 20]

    // ── enumerate, zip, chain ──────────────────────────────────────────
    for (i, value) in numbers.iter().enumerate() {
        if i > 2 { break; }
        print!("[{i}]={value} ");
    }
    println!();

    let names = vec!["ada", "grace", "alan"];
    let years = vec![1815, 1906, 1912];
    for (name, year) in names.iter().zip(years.iter()) {
        println!("{name} ({year})");
    }

    let chained: Vec<i32> = (1..=3).chain([10, 20]).collect();
    println!("{chained:?}");             // [1, 2, 3, 10, 20]

    // ── fold / reduce: the universal accumulator ───────────────────────
    let sum = numbers.iter().fold(0, |acc, &n| acc + n);
    println!("sum = {sum}");             // 55

    // ── find, any, all, position ───────────────────────────────────────
    let first_big = numbers.iter().find(|&&n| n > 7);
    println!("{first_big:?}");           // Some(8)
    println!("{}", numbers.iter().any(|&n| n == 5));   // true
    println!("{}", numbers.iter().all(|&n| n > 0));    // true
    println!("{:?}", numbers.iter().position(|&n| n == 4)); // Some(3)

    // ── sorting & grouping ─────────────────────────────────────────────
    let mut words = vec!["pear", "apple", "fig"];
    words.sort_by_key(|w| w.len());      // by length
    println!("{words:?}");               // ["fig", "pear", "apple"]

    let (min, max) = numbers.iter().fold((i32::MAX, i32::MIN), |(lo, hi), &n| {
        (lo.min(n), hi.max(n))
    });
    println!("min={min} max={max}");

    // ── infinite iterators + take = finite work ────────────────────────
    let squares: Vec<u64> = (1u64..)
        .map(|n| n * n)
        .take(5)
        .collect();
    println!("{squares:?}");             // [1, 4, 9, 16, 25]

    // ── chunks, windows, step_by ───────────────────────────────────────
    println!("{:?}", numbers.chunks(3).collect::<Vec<_>>());
    println!("{:?}", numbers.windows(3).collect::<Vec<_>>());
    println!("{:?}", numbers.iter().step_by(3).collect::<Vec<_>>());
}

Ownership modes of iteration

MethodYieldsOwnershipUse when
iter()&TBorrowsReading values, by far the most common
iter_mut()&mut TMutable borrowModifying each element in place
into_iter()TConsumesTaking ownership (moves each element out)
for x in &collection&TBorrowsSugar for collection.iter()
Note: for x in collection consumes the collection. If you need it afterwards, iterate with &collection.
💡 Iterators are not slow

Because of monomorphisation and inlining, this chain:

let total: i32 = data.iter().filter(|&&x| x > 0).map(|x| x * 2).sum();

compiles to essentially the same assembly as a manual for loop with an if and a multiply. In benchmarks it is frequently faster than C++ STL iterators because the compiler can reason about the whole pipeline at once. "Zero-cost abstraction" is not marketing — it is measured.

Chapter 13

Smart pointers & interior mutability

When plain references and ownership aren't enough — shared ownership, recursive types, dynamic dispatch — Rust offers explicit, self-documenting smart pointers. Each one tells you exactly what memory behaviour to expect.

Box<T> — heap allocation

boxing.rs
use std::mem;

fn main() {
    // 1. A value on the heap, with a stack pointer to it
    let b = Box::new(5);
    println!("{b}");
    // `b` behaves exactly like the i32 it holds, thanks to Deref
    println!("{}", b + 1);      // 6

    // 2. BREAKING infinite-size recursion
    // This would be infinitely large, so it's a compile error:
    // enum List { Cons(i32, List), Nil }
    // Box gives the type a known, finite size:
    #[derive(Debug)]
    enum List { Cons(i32, Box<List>), Nil }
    use List::{Cons, Nil};
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("{list:?}");

    // 3. Trait objects: dynamically sized values
    let shape: Box<dyn std::fmt::Display> = Box::new("a boxed string");

    // 4. Large data you want off the stack
    let big = Box::new([0u8; 1_000_000]);

    // A Box is 8 bytes on the stack (just the pointer)
    println!("size of Box: {}", mem::size_of_val(&b));    // 8
    println!("size of List: {}", mem::size_of_val(&list)); // 16
}

Rc<T> & Arc<T> — shared ownership

Normally a value has one owner. Rc (reference-counted) allows many: it keeps a count on the heap, and the value is freed when the count reaches zero. Arc is the same but atomically thread-safe (slightly slower).

What Rc<String> sharing looks like on the heap
Two stack handles, one heap value
a: Rc<String>ptr ─┐
b: Rc<String>ptr ─┤
↓
RcBox { strong: 2, weak: 0 }ref counts
value: String "data"freed when strong == 0
Rules & trade-offs
Rc::clone()O(1): just bumps a counter
Rc::strong_count(&a)inspect the count
mutable access❌ Rc<T> is read-only
threads❌ not Send — use Arc
Rc is single-threaded shared ownership (GUI trees, graphs, DAGs). Arc is its multi-threaded sibling, used constantly in concurrent code.
rc_arc.rs
use std::rc::Rc;
use std::sync::Arc;
use std::thread;

fn main() {
    // ── Rc: shared ownership on one thread ──────────────────────────────
    let shared = Rc::new(String::from("shared data"));
    let a = Rc::clone(&shared);      // note: Rc::clone, not .clone() — makes the intent visible
    let b = Rc::clone(&shared);
    println!("{} {} {}", shared, a, b);
    println!("refcount = {}", Rc::strong_count(&shared));   // 3

    // ── Arc: the same, but safe to send across threads ─────────────────
    let numbers = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = vec![];
    for i in 0..3 {
        let nums = Arc::clone(&numbers);
        handles.push(thread::spawn(move || {
            let sum: i32 = nums.iter().sum();
            println!("thread {i} sums to {sum}");
        }));
    }
    for h in handles { h.join().unwrap(); }
}

RefCell<T> — interior mutability

RefCell lets you mutate a value even when you only hold a shared reference — by moving the borrow check from compile time to runtime. Break the rules and your program panics instead of corrupting memory.

refcell.rs
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    // Runtime borrow rules: same as compile-time, but checked when you call
    {
        let mut borrowed = data.borrow_mut();   // exclusive runtime borrow
        borrowed.push(4);
    }                                           // borrow ends here
    println!("{:?}", data.borrow());            // [1, 2, 3, 4]

    // data.borrow_mut() while borrowed → 💥 panic: already borrowed

    // The classic combination: Rc<RefCell<T>> = shared AND mutable
    let shared_list = Rc::new(RefCell::new(Vec::new()));
    let a = Rc::clone(&shared_list);
    let b = Rc::clone(&shared_list);
    a.borrow_mut().push(10);
    b.borrow_mut().push(20);
    println!("{:?}", shared_list.borrow());     // [10, 20]
}

Interior mutability in the standard library

TypeThreadsCostTypical use
Cell<T>Single (not Sync)NoneCopy types: counters, flags. .get() / .set()
RefCell<T>SingleRuntime checksComplex values needing mutation through a shared ref
Mutex<T>MultiLockingShared mutable state across threads
RwLock<T>MultiLockingMany readers, occasional writer
Atomic*MultiAtomic opsCounters, flags, lock-free structures
OnceCell / OnceLockBothOne-time initGlobal singletons, lazy config

Deref & Drop — how smart pointers behave like plain ones

deref_drop.rs
use std::ops::Deref;
use std::fmt;

// A custom smart pointer
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> { MyBox(x) }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }      // enables `*my_box` and method auto-deref
}

impl<T: fmt::Debug> Drop for MyBox<T> {
    fn drop(&mut self) {
        println!("dropping MyBox with {:?}", self.0);
    }
}

fn hello(name: &str) { println!("Hello, {name}!"); }

fn main() {
    let x = MyBox::new(String::from("Rust"));
    hello(&x);          // auto-deref: &MyBox<String> → &String → &str
                        // (the compiler inserts derefs — zero runtime cost)
}                       // "dropping MyBox with ..." printed HERE, deterministically
⚠️ Reference cycles leak memory (even in Rust)

Two Rcs pointing at each other keep their reference counts above zero forever. The ownership system prevents unsafety, not leaks. Fix it with Weak<T> for the "child → parent" direction — a non-owning link that doesn't increase the strong count:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>> }

let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]) });
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);   // weak link: no cycle

Which smart pointer should I use?

I need…Use
Heap-allocated value, single ownerBox<T>
Shared ownership, one threadRc<T>
Shared ownership, many threadsArc<T>
Shared and mutable, one threadRc<RefCell<T>>
Shared and mutable, many threadsArc<Mutex<T>> or Arc<RwLock<T>>
A copy-on-write string/vecCow<'a, T>
A lazily-initialised globalOnceLock<T> / LazyLock
A non-owning back-referenceWeak<T>
Reaching for Rc/Arc too early is a common mistake. Start with plain ownership and references — they're simpler and faster — and add sharing only when the design truly demands it.
Chapter 14

Fearless concurrency

Rust's most quotable claim: "fearless concurrency". The same ownership and type rules that guarantee memory safety also make data races a compile error. No other mainstream language does this.

Why this matters

A data race is when two threads access the same memory concurrently, at least one writes, and there's no synchronisation. In C++ the result is undefined behaviour — a heisenbug that vanishes in the debugger. In Go/Java it corrupts state at runtime. In Rust, it simply does not compile.

🧵

Threads (std::thread)

OS threads, one per core. Best for CPU-bound work. Fully safe and supported on stable Rust today.

📬

Message passing (channels)

"Don't communicate by sharing memory; share memory by communicating." mpsc gives you multi-producer, single-consumer channels in the standard library.

🔒

Shared state (Mutex / RwLock)

When you must share, wrap the value in a lock. The type system forces you to acquire the lock before touching the data.

⚡

Async / await

For thousands of concurrent I/O tasks (servers, clients). Managed by an executor such as tokio or async-std.

Spawning threads

threads.rs
use std::thread;
use std::time::Duration;

fn main() {
    let mut handles = vec![];

    for i in 0..4 {
        // `move` transfers ownership of `i` into the thread — required because
        // the thread may outlive the current scope.
        let handle = thread::spawn(move || {
            for step in 0..3 {
                println!("thread {i}: step {step}");
                thread::sleep(Duration::from_millis(10));
            }
            i * 10                    // the closure's return value
        });
        handles.push(handle);
    }

    // Join: wait for each thread and collect its result
    for h in handles {
        let result = h.join().unwrap();
        println!("thread returned {result}");
    }

    // Scoped threads (Rust 1.63+): borrow data safely without `move`,
    // guaranteed to finish before the scope ends.
    let data = vec![1, 2, 3, 4];
    thread::scope(|s| {
        let half = &data[..2];
        s.spawn(|| println!("first half: {half:?}"));
        s.spawn(|| println!("sum: {}", data.iter().sum::<i32>()));
    });
    // `data` is still usable here — the threads were scoped
    println!("{data:?}");
}

Message passing with channels

channels.rs
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();          // transmitter, receiver

    // Multiple producers: just clone the transmitter
    let tx2 = tx.clone();
    thread::spawn(move || {
        let messages = vec![String::from("hi"), String::from("from")];
        for m in messages {
            tx.send(m).unwrap();
            thread::sleep(Duration::from_millis(20));
        }
    });
    thread::spawn(move || {
        tx2.send(String::from("the thread!")).unwrap();
    });

    // rx.recv() blocks until a message arrives; try_recv() returns Result immediately.
    // When all senders drop, iteration ends cleanly.
    for received in rx {
        println!("got: {received}");
    }
}

Shared state with Mutex<T> and Arc<T>

mutex.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc for shared ownership across threads, Mutex for safe mutation
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();   // acquire the lock (guard)
            *num += 1;
            // lock released automatically when `num` (the MutexGuard) drops here
        }));
    }

    for h in handles { h.join().unwrap(); }
    println!("result = {}", *counter.lock().unwrap());   // 100
}
💡 The lock guard is a RAII value

counter.lock() returns a MutexGuard. It unlocks in its Drop implementation — meaning the lock is always released, even if your code panics or returns early. There is no way to forget to unlock, and no way to hold a lock after the data is gone.

Send and Sync — the traits behind "fearless"

What the compiler actually enforces
Send"Can this value be moved to another thread?" Auto-implemented for almost everything except Rc, RefCell, raw pointers.
→
Sync"Can this value be referenced from multiple threads at once?" T: Sync ⟺ &T: Send. Mutex<T> and Arc<T> are Sync; RefCell is not.
→
Resultthread::spawn requires Send + 'static. If your type isn't safe to share, the compiler says so at the spawn site — not in a crash report six weeks later.
// ❌ This is a compile error — Rc is not Send:
use std::rc::Rc;
use std::thread;
let r = Rc::new(5);
// thread::spawn(move || println!("{r}"));   // error: `Rc<i32>` cannot be sent between threads safely
// Fix: use std::sync::Arc instead — the fix is visible in the type.

Async / await

Threads are perfect for CPU work, but a server handling 10,000 simultaneous connections can't afford 10,000 OS threads. Async Rust lets one thread drive thousands of tasks, each of which pauses at .await points and resumes later.

async_example.rs — Cargo.toml: tokio = { features = ["full"] }
use tokio::time::{sleep, Duration};

// `async fn` returns a Future: a lazily-evaluated state machine, NOT a result
async fn fetch_user(id: u32) -> String {
    sleep(Duration::from_millis(100)).await;   // yields control back to the executor
    format!("user-{id}")
}

async fn fetch_all() -> Vec<String> {
    // Run both concurrently and await both results
    let (a, b) = tokio::join!(fetch_user(1), fetch_user(2));
    vec![a, b]
}

// The `#[tokio::main]` macro builds the runtime (executor + I/O driver + timer)
#[tokio::main]
async fn main() {
    let users = fetch_all().await;            // .await drives the future to completion
    println!("{users:?}");

    // Spawn independent tasks that run concurrently
    let mut handles = vec![];
    for i in 0..100 {
        handles.push(tokio::spawn(async move {
            fetch_user(i).await
        }));
    }
    for h in handles {
        println!("{}", h.await.unwrap());
    }
}

Threads vs async — when to use which

OS ThreadsAsync / Future
Best forCPU-bound work: parsing, encoding, maths, compressionI/O-bound work: servers, websockets, file/network polling
Concurrency limit~Hundreds (each is an OS resource)Millions of tasks per process
Memory per task~8 MB stack (reserved) per thread~Hundreds of bytes per future
SchedulingOS kernelExecutor (tokio, async-std, smol)
Code stylePlain synchronous codeasync/.await, "coloured functions"
Blocking is…ExpectedForbidden — it stalls the whole executor
MaturityComplete on stableStable syntax; ecosystem still converging
Golden rule: never call blocking code inside async fn. Use tokio::task::spawn_blocking to push CPU-heavy work onto a separate thread pool.
⚠️ The honest state of async Rust

Async Rust is powerful but genuinely more complex: Send bounds on futures, Pin, the "coloured function" problem (async functions can only be called from async contexts), and no async traits in dyn until recently. Learn synchronous Rust first. Come back to async when you're building a real network service — by then the type system will feel like an ally.

Data parallelism in one line

rayon.rs — Cargo.toml: rayon = "1"
use rayon::prelude::*;

fn main() {
    let mut data: Vec<u64> = (0..10_000_000).collect();

    // Just change `.iter()` to `.par_iter()` — that's the whole migration.
    // Rayon splits the work across all CPU cores automatically.
    let sum: u64 = data.par_iter().map(|x| x * x).sum();

    data.par_sort_unstable();     // parallel sort, often 4-8x faster

    println!("{sum}");
}
Chapter 15

Unsafe Rust & foreign function interface

Rust's guarantees are opt-out, not opt-in. unsafe gives you five extra abilities — and shifts the correctness burden back onto you, in a small, greppable amount of code.

What unsafe unlocks

👉

1. Dereference raw pointers

*const T and *mut T. Raw pointers may alias, may be null, may dangle — all bets are off.

📞

2. Call unsafe functions

Includes calling into C code via extern "C". The caller must uphold the documented invariants.

🏷️

3. Access mutable statics

static mut globals. Inherently racy across threads — prefer Atomic* or OnceLock.

📜

4. Implement unsafe traits

Traits where incorrect implementation causes UB, like Send, Sync, or GlobalAlloc.

5. Access fields of a union

Unions let several fields share the same memory. Reading the wrong variant is undefined behaviour, so it requires unsafe.

🧩 What unsafe does NOT turn off

The borrow checker still runs. In an unsafe block you cannot suddenly have two &mut references to the same variable. unsafe unlocks five specific abilities — it is not a "disable all checks" switch. Also: the block is small, explicit, and greppable (rg "unsafe"), so you know exactly where the risk lives. Most crates contain zero unsafe.

unsafe.rs
fn main() {
    let mut num = 5;

    // Creating raw pointers is SAFE. Dereferencing them is not.
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 = {r1:?}, value = {}", *r1);   // deref requires unsafe
        *r2 = 10;                                   // write through a raw pointer
    }
    println!("num = {num}");                        // 10

    // Calling an unsafe function
    unsafe fn dangerous() {
        // …the contract must be documented and upheld by every caller
    }
    unsafe { dangerous(); }

    // Safe abstraction over unsafe internals — the idiomatic pattern.
    // The `unsafe` is contained in ONE small function with a documented invariant.
    fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
        let len = slice.len();
        let ptr = slice.as_mut_ptr();
        assert!(mid <= len);
        unsafe {
            (
                std::slice::from_raw_parts_mut(ptr, mid),
                std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
            )
        }
    }

    let mut v = vec![1, 2, 3, 4];
    let (a, b) = split_at_mut(&mut v, 2);
    println!("{a:?} {b:?}");                        // [1, 2] [3, 4]
}

Calling C (and being called by C)

ffi.rs
// Declare an external C function (linked at build time)
extern "C" {
    fn abs(input: i32) -> i32;
    fn sqrt(input: f64) -> f64;
}

// Make a Rust function callable from C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

// Layout compatible with C structs
#[repr(C)]
pub struct CPoint {
    pub x: f64,
    pub y: f64,
}

// A callback that C code can invoke
pub type Callback = extern "C" fn(i32) -> i32;

fn main() {
    unsafe {
        println!("abs(-3) = {}", abs(-3));
        println!("sqrt(2) = {:.4}", sqrt(2.0));
    }
}
🧰

bindgen

Generates Rust FFI bindings automatically from C header files. The standard way to consume an existing C library.

📤

cbindgen

Generates C headers from your Rust code, so C/C++/Python/Node projects can call into your library.

🔌

PyO3 / Neon / napi-rs

Write Python extensions, Node native modules or C# bindings in Rust with a safe, ergonomic API.

📦

cargo-c

Builds your Rust crate as a C-compatible shared/static library with a proper .pc file.

💡 Tooling for unsafe code

Miri (cargo +nightly miri test) interprets your code and detects undefined behaviour, use-after-free, and invalid pointer arithmetic at test time. ASan/LSan/TSan (address/leak/thread sanitizers) work with the nightly toolchain. cargo-geiger reports how much unsafe every one of your dependencies contains. If you write unsafe, these are not optional.

Chapter 16

Tooling, testing & documentation

Rust's tooling is arguably its greatest hidden feature. Everything you need is built in and works out of the box: formatter, linter, test runner, documentation generator, package manager and benchmark harness.

Unit tests in the same file

src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }

pub fn greeting(name: &str) -> String {
    format!("Hello, {name}!")
}

// Tests live in a module gated behind #[cfg(test)] — compiled ONLY for `cargo test`
#[cfg(test)]
mod tests {
    use super::*;                     // import everything from the parent module

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 2), 5);
    }

    #[test]
    fn greeting_contains_name() {
        let g = greeting("Rust");
        assert!(g.contains("Rust"), "greeting was: {g}");
    }

    #[test]
    #[should_panic(expected = "divided by zero")]
    fn test_panic() {
        let _ = 1 / 0;
    }

    #[test]
    #[ignore]                         // skip unless run with: cargo test -- --ignored
    fn expensive_test() { }

    // Returning Result from a test lets you use `?` — elegant for fallible tests
    #[test]
    fn returns_result() -> Result<(), std::num::ParseIntError> {
        let n: i32 = "42".parse()?;
        assert_eq!(n, 42);
        Ok(())
    }
}

Integration tests, doc tests & coverage

tests/integration.rs
// tests/ holds BLACK-BOX tests: they use your crate as an external user would.
use my_library;                        // the crate name

#[test]
fn full_workflow() {
    assert_eq!(my_library::add(2, 2), 4);
}
doc comments are tested automatically
/// Adds two numbers.
///
/// # Examples
///
/// ```
/// let result = my_library::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

//! Crate-level documentation (goes at the top of lib.rs)
//! ```
//! use my_library::add;
//! ```
terminal
cargo test                       # run everything (unit + integration + doc)
cargo test test_add              # filter by name
cargo test -- --threads=1        # single-threaded output is easier to read
cargo test -- --nocapture        # show println! output
cargo test --release             # test optimised code too
cargo tarpaulin --out Html       # code coverage report (install: cargo install cargo-tarpaulin)
cargo llvm-cov                   # faster coverage using LLVM instrumentation

Documentation as a first-class citizen

ToolCommandWhat it does
Local docscargo doc --openBuilds HTML docs for your code and all dependencies, opens in browser
Public docsdocs.rsAuto-generated for every published crate
Lintingcargo clippy500+ idiomatic lints with explanations and auto-fixes (--fix)
Formattingcargo fmtOfficial style guide, deterministic, zero arguments with your team
Language serverrust-analyzerInline type hints, refactoring, macro expansion, quick fixes
Debugginglldb / gdb / IDE debuggersFull source-level debugging with pretty printers for Rust types
Benchmarkscargo bench (criterion)Statistical benchmarking with regression detection and charts
Macro inspectioncargo expandShows exactly what #[derive] and macros generate
Unsafe analysiscargo geigerCounts unsafe usage across your whole dependency tree
Auditcargo audit / cargo denyKnown vulnerabilities, duplicate deps, licence violations
Releasecargo publishUploads to crates.io (runs your tests first)

Release profile tuning

Cargo.toml
[profile.release]
opt-level = 3          # 0-3, or "s"/"z" for size optimisation
lto = "fat"            # link-time optimisation: slower build, faster binary
codegen-units = 1      # maximum optimisation across the whole crate
panic = "abort"        # smaller binary (no unwinding machinery)
strip = true           # remove debug symbols from the final binary
incremental = false    # release builds don't need incremental compilation

[profile.dev]
opt-level = 0          # fast compilation for the inner loop
debug = true           # keep symbols for the debugger
~500+
Clippy lints
1 cmd
cargo fmt, zero config
100%
of doc examples are tests
0
external build tools needed
💡 The three-command hygiene habit

Before every commit, run:

cargo fmt --check && cargo clippy -- -D warnings && cargo test

Wire it into a git pre-commit hook or CI (GitHub Actions has a one-line dtolnay/rust-toolchain setup) and your codebase will stay consistent for years.

Chapter 17

Performance & how Rust compares to other languages

Claims are cheap. This chapter shows why Rust is fast, then compares measured numbers across execution speed, memory, binary size, startup latency, web throughput and compile time — including the places where Rust loses.

🧭 How to read these charts

The figures below are illustrative order-of-magnitude ranges drawn from publicly published benchmarks (The Computer Language Benchmarks Game, TechEmpower Web Framework Benchmarks, and vendor engineering blogs). Real numbers vary with hardware, compiler version, and how carefully each program is written. Use them to understand relative scale, not to score points.

Why Rust is fast — the six reasons

🚫

No garbage collector

Zero background CPU usage, zero pause spikes, zero heap metadata overhead. Memory is freed at a compile-time-known point. GC languages typically use 2–5× the memory and lose 15–30% of CPU to collection.

🧬

Monomorphised generics

Generic code is specialised per concrete type at compile time. Java erases types and boxes; Rust inlines them into direct calls.

🏗️

LLVM backend

Rust compiles through the same optimiser as Clang/C++: inlining, vectorisation, loop unrolling, SIMD, LTO. Decades of optimisation research, free.

🧊

Predictable memory layout

Structs are laid out contiguously like C arrays, not as scattered objects. Arrays of Vec3 iterate with perfect cache locality, unlike arrays of pointers-to-objects in Java.

👁️

No hidden allocations

Nothing allocates behind your back. No boxing for interfaces, no string concatenation temporaries, no autoboxing of integers. You see every Vec::new().

🧵

Fearless parallelism

Because the compiler proves thread safety, you can parallelise aggressively — rayon turns a serial iterator into a multi-core one in a single token change.

Relative execution time — CPU-bound workloads · the native & managed tier
Lower is better · normalised so C = 1.0 · linear scale · averaged across numeric, allocation-heavy and string benchmarks
C
1.00×
C++
1.04×
Rust
1.07×
Zig
1.10×
C#
1.5×
Java
1.6×
Go
2.1×
C, C++, Rust and Zig are statistically in the same tier — the differences between them are smaller than the difference between two C++ compilers. Managed languages are a modest constant factor behind. Scripting languages are a different universe entirely (next chart).
The same benchmark, all the way down to scripting languages
Lower is better · logarithmic scale (a 70× gap cannot be drawn linearly and still show C) · baseline = Rust
C++
0.97×
Rust
1.00×
C#
1.4×
Java
1.5×
Go
2.0×
JS (V8)
5.0×
Python
~70×
C C++ Rust Zig C# Java Go JavaScript Python

Reading the chart: Rust sits in the same tier as C and C++ — within a few percent on most workloads, occasionally faster thanks to stronger aliasing information (&mut tells LLVM that data does not alias, something C's restrict only hints at). Go is a small constant slower due to its runtime and GC. Managed languages pay for JIT warm-up and heap indirection. Python/JS are a different category entirely.

Peak memory usage — allocating workload (binary-trees style)
Lower is better · relative to C · linear scale · garbage-collected languages need large headroom to collect efficiently
C
1.00×
Rust
1.05×
C++
1.10×
Java
3.0×
C#
3.3×
Go
3.5×
Beyond this chart: JavaScript (V8) typically needs ~8× and CPython ~15–30× the memory of C for the same work — every integer and small object carries heap metadata.
This is why Rust is chosen for containers and edge functions: a Rust service runs comfortably in 30 MB where the equivalent Go or Java service needs 200 MB+. Smaller footprints mean denser deployments and lower cloud bills.
Web service throughput — JSON serialisation + HTTP (TechEmpower-style)
Higher is better · approximate requests/second on a single 8-core commodity server · framework choice matters as much as language
Rust (axum)
~700k
C++ (drogon)
~640k
Java (vertx)
~520k
C# (ASP.NET)
~500k
Go (fasthttp)
~410k
Node (fastify)
~150k
Python (FastAPI)
~50k
A naïve implementation in Rust will lose to a tuned one in Go. The point is not that Rust always wins — it's that Rust's ceiling is the highest of any safe language, and it reaches it without unsafe.
Where Rust is NOT the fastest: clean build time
Lower is better · time to compile a medium-sized project (~10k lines + 50 dependencies) from scratch · Rust's main honest weakness
Go
12s
C
20s
C#
28s
Java
40s
Rust
75s
C++
95s
Mitigations that work: cargo check instead of build in your inner loop (3–5× faster), incremental compilation (already on), sccache, cargo-nextest, splitting into a workspace of small crates, and cranelift as a fast backend for debug builds (cargo +nightly build -Zcodegen-backend=cranelift).
Cold start & latency predictability
Higher is better · normalised score combining startup time, binary size and tail-latency stability under load
C
9.6
Rust
9.4
C++
9.1
Go
7.2
C#
5.2
Java
4.8
JS (Node)
6.2
Python
4.0
No GC means no latency spikes — critical for audio processing, game frame budgets, high-frequency trading, and tail-latency-sensitive services (p99.9).

The case study everyone quotes: Discord

📊 From Go to Rust — measured, not marketing

Discord's Read States service was written in Go and suffered latency spikes: every few seconds, garbage collection would stop the world and the response time graph showed visible "sawtooth" spikes. They rewrote the hot path in Rust and the spike simply disappeared — the latency graph became flat, and the same hardware handled more load with less memory.

Their conclusion: "Rust gives us the performance of C++ with the safety of Go, and the compiler refuses to ship code with memory errors." Similar stories come from Cloudflare (Pingora replaced NGINX with ~70k lines of Rust and cut CPU usage), Figma (multiplayer server), and 1Password.

Rust vs C++ — the closest competitor

AreaRustC++Verdict
Raw performanceExcellentExcellentTie
Memory safetyGuaranteed by the compilerManual; UB is everywhereRust
Undefined behaviourImpossible in safe codeRampant and often invisibleRust
Compile timesSlowVery slow (headers, templates)Rust slightly
Toolingcargo: one unified toolCMake, Conan, vcpkg, Make… fragmentedRust
Package ecosystemcrates.io: 150k+ crates, consistentHuge but fragmented across registriesRust cohesion
Legacy / library depthGrowing40 years of everythingC++
Language complexityLarge, but consistentEnormous, many traps and legacy cornersRust
Job marketSmaller, high-paying, growing fastMassive, establishedC++ today
Learning curveSteep then smoothModerate to learn, endless to masterDepends
InteroperabilityExcellent C FFINative C++ ABIC++ for C++ code
Use Rust instead of C++ when: you're starting something new, correctness matters, and you don't need a specific C++-only library or ABI. Stay with C++ when: the codebase is enormous and already works, or you depend on Unreal Engine / Qt / deep C++ frameworks.

Rust vs Go

AreaRustGo
Design goalMaximum performance + safetyMaximum simplicity + fast builds
Learning curveSteep (ownership, traits, lifetimes)Gentle — teachable in a week
Memory modelOwnership, no GCTracing GC, no manual control
Speed / memoryFaster, far less memory~1.5–2.5× slower, 3–4× more RAM
ConcurrencyThreads + async, compile-time checkedGoroutines — beautifully simple
Compile speedSlowVery fast
Error handlingResult + ?, typed, exhaustiveif err != nil — simple but repetitive
AbstractionGenerics + traits + macrosMinimal by design
Best forSystems, performance-critical, embedded, CLI, WasmNetwork services, infrastructure, CLIs, quick delivery
Honest take: Go is easier to hire for and faster to ship simple services in. Rust wins when performance, memory footprint, correctness guarantees, or long-term maintainability are the priority. Many teams use both.

Rust vs managed languages (Java, C#, Python, JS/TS)

ConcernRustJava / C#Python / JS
Execution speedNative, top tierFast after JIT warm-up10–100× slower
Memory footprintTinyLarge (VM + heap)Moderate–large
StartupMilliseconds100 ms – secondsFast, but slow work after
Latency predictabilityDeterministicGC pauses (much better with ZGC/G1)GC pauses, jitter
Concurrency scalingExcellent (no GIL)Excellent (virtual threads in modern Java)Python: GIL-limited · JS: single-threaded event loop
Productivity / prototypingSlower initiallyFast with good toolingFastest
Ecosystem breadthStrong in systems domainsMassive, enterprise-gradeMassive, especially web/ML
DeploymentSingle static binaryNeeds runtime (or AOT)Needs interpreter/Node
Hiring poolSmaller, highly skilledVery largeLargest

Beyond speed: the other measurable wins

~70%
of Chrome/Windows CVEs were memory-safety bugs Rust eliminates
0
memory-safety CVEs in Rust's own std library
2×
denser container packing from smaller RAM use
1 file
to deploy: a static binary with no runtime
~50%
of Android's new native code is now Rust

Binary size & distribution

"Hello world" binary size
Lower is better · release build, stripped · square-root scale (15 KB and 1.5 MB cannot share a linear axis meaningfully) · Rust and Go ship a self-contained executable
C
~15 KB
C++
~45 KB
Rust
~300 KB
Go
~1.5 MB
Java and C# are not shown because they ship bytecode plus a runtime rather than a native executable — a different distribution model.
Rust binaries can be shrunk dramatically with opt-level = "z", LTO, panic = "abort", strip = true, and a tiny #[global_allocator] such as dlmalloc. For WebAssembly, size matters enormously — and Rust is the leading Wasm language.

When Rust is the wrong choice

⚡

Rapid prototyping

If you need a throwaway script or MVP by tonight, Python or TypeScript will beat you there. Rust's guarantees cost design time up front.

📚

Very large existing codebases

Rewriting working Java/C++/Python services "for performance" is usually a bad trade. Rewrite only measured hot paths.

🧩

Domains with dominant ecosystems

Data science, ML training and web front-ends are dominated by Python and JavaScript respectively. Rust can accelerate the slow parts (see PyO3, Polars, Leptos) but is not the default.

👥

Teams that can't invest in learning

Rust pays off over years. If the team is 3 people shipping a 6-month product with high turnover, the ramp-up cost may dominate.

⚠️ The "rewrite it in Rust" trap

Performance problems are usually architectural — an N+1 query, a bad algorithm, excessive I/O — not a language problem. Always measure first. The right outcome of profiling is often "move 5% of the code to Rust" (a WASM module, an FFI extension) rather than a full rewrite.

Side-by-side: the same task in 6 languages

Count word frequencies — a task that touches strings, collections, iteration and error handling:

word_count.rs
use std::collections::HashMap;

fn main() {
    let text = "the quick brown fox jumps over the lazy dog the fox";

    let counts: HashMap<&str, usize> = text
        .split_whitespace()
        .fold(HashMap::new(), |mut acc, word| {
            *acc.entry(word).or_insert(0) += 1;
            acc
        });

    let mut sorted: Vec<_> = counts.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1));

    for (word, n) in sorted {
        println!("{word:>6}: {}", "█".repeat(n));
    }
}
LanguageApprox. linesSpeedNotes
Rust14Baseline (fastest)Explicit, typed, no allocations hidden
C++16~sameunordered_map, stringstream
Go15~2× slowerVery readable, map[string]int
Python7~40× slowercollections.Counter — wonderfully terse
JavaScript11~5× slowerMap, reduce
Java20~1.6× slowerVerbose stream pipeline
Notice the trade-off clearly: Python is half the lines and 40× slower. Rust is more verbose than Python but runs at C speed with no runtime. Choose based on where your bottleneck is.
Chapter 18

Reading the compiler's mind

Rust's compiler is famous for being helpful. It doesn't just say "error" — it explains the rule, points at the exact line, and frequently hands you the fix. Here's how to read its output and the ten errors you will absolutely meet.

How to read a Rust error message

rustc output
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:15
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s2}");
5 |     println!("{s1}");
  |               ^^^^ value borrowed here after move
  |
  = note: move occurs because `s1` has type `String`, which does not implement the `Copy` trait
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                   ++++++
Line 1The error code and title. Search for E0382 in the Rust Error Index for a full explanation.
→
The gutterA code diagram. It shows the relevant lines and underlines the exact span, with arrows explaining the history of the problem.
→
= noteThe underlying rule — here, why a move happened rather than a copy.
→
help:A suggested patch, with + markers showing exactly what to add. Often one keystroke with rust-analyzer.

The 10 errors every beginner meets

E0382 — use of moved value
let s = String::from("hi");
let t = s;
println!("{s}");   // error: borrow of moved value: `s`

Why: String owns heap memory; assignment moves ownership to t, invalidating s.

Fixes: use t instead · clone: let t = s.clone(); · borrow: let t = &s; · restructure so the function returns the value back.

E0384 — cannot assign twice to immutable variable
let x = 5;
x = 6;   // error: cannot assign twice to immutable variable `x`

Why: let bindings are immutable by default — this is a feature.

Fixes: let mut x = 5; · or shadow: let x = 6; (creates a new binding).

E0502 / E0499 — cannot borrow as mutable (also as immutable)
let mut v = vec![1, 2, 3];
let first = &v[0];     // shared borrow starts
v.push(4);             // error[E0502]: cannot borrow `v` as mutable
println!("{first}");   // …because the shared borrow is used here

Why: one mutable borrow XOR many shared borrows. This is what prevents data races.

Fixes: copy the value out (let first = v[0];) · finish using the shared borrow first · restructure the code into sequential phases · use split_at_mut to borrow two disjoint parts.

E0282 — type annotations needed
let x = "5".parse().unwrap();   // error: type annotations needed
let y: i32 = "5".parse().unwrap();            // ✅ annotate
let z = "5".parse::<i32>().unwrap();          // ✅ turbofish

Why: parse can produce any type implementing FromStr, and there is no other clue.

E0277 — the trait bound is not satisfied
#[derive(Debug)]
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
println!("{}", p);   // error: `Point` doesn't implement `std::fmt::Display`

Why: {} requires Display; Point only derives Debug.

Fixes: use {:?} · or impl fmt::Display for Point by hand (it can't be derived).

General form: "the trait bound X: Trait is not satisfied" — either implement Trait for X, or relax the bound on your function.

E0597 / E0596 — does not live long enough / cannot borrow as mutable
let r;
{
    let x = 5;
    r = &x;    // error: `x` does not live long enough
}
println!("{r}");

Why: the reference would outlive the value. Rust forbids dangling pointers at compile time.

Fixes: declare x in the outer scope · return the value instead of a reference · use String/Vec and move the data.

E0599 — no method named X on type Y

Why: The method doesn't exist, or its trait isn't in scope. Very common with Iterator, Read, Write, FromStr.

Fixes: check the spelling · add use std::io::Read; (or the relevant trait) · check you're not holding &T when you need &mut T · check the docs for the correct method name.

E0308 — mismatched types
let x: i32 = 5;
let y: f64 = x;      // error: expected `f64`, found `i32`
let y = x as f64;    // ✅ explicit cast

Why: Rust never implicitly converts numeric types — no hidden truncation or overflow.

Fixes: as cast · .parse() · Into/From conversions · change the annotation.

E0601 — main function not found

Why: you're building a binary crate without fn main().

Fixes: add fn main() {} · or add [lib] to Cargo.toml if you meant to build a library.

"temporary value dropped while borrowed"
let s = String::from("a").split('.').next().unwrap();
// error: temporary value dropped while borrowed

Why: the temporary String created inline is destroyed at the end of the statement, but you kept a borrow of it.

Fixes: bind it first: let raw = String::from("a"); let s = raw.split('.').next().unwrap();

Chapter 19

Projects & a realistic study plan

You cannot learn Rust by reading alone. This chapter gives you a 30-day plan and projects that each teach exactly one hard concept, in the order that makes them stick.

A 30-day study plan

DaysFocusDo thisYou'll be able to…
1–3Tooling & syntaxInstall rustup, read Chapters 2–4, run everything in the PlaygroundWrite functions, use let/mut, loops, match
4–7Ownership (hard!)Chapter 5 twice. Deliberately trigger E0382/E0502 and read the messagesExplain move vs copy, borrow rules, slices
8–10Structs & enumsChapter 7. Build a tiny CLI calculatorModel data with enums, use match exhaustively
11–14Collections & errorsChapters 8–9. Build a word-frequency counterUse Vec/HashMap, propagate errors with ?
15–18Generics & traitsChapter 10. Implement Display + From for your typesWrite generic functions, understand bounds
19–21Modules & cargoChapter 11. Split your project into modules, publish docsStructure a real project
22–24Closures & iteratorsChapter 12. Refactor loops into iterator chainsWrite idiomatic, fast functional code
25–27Smart pointersChapter 13. Build a doubly-linked list / tree with Rc<RefCell>>Know when to reach for Box/Rc/Arc
28–29ConcurrencyChapter 14. Parallelise a data pipeline with rayonSpawn threads, use Arc<Mutex>, channels
30Review & shipTake a project from idea to cargo publish --dry-runShip a complete, tested, documented Rust crate

Project ladder — each one teaches a specific concept

🟢

1. Temperature converter CLI

Concepts: std::env::args, parsing, Result, basic functions. An afternoon's work.

🟢

2. Guessing game

Concepts: rand crate, loop, match on cmp::Ordering, Option, user input. It's the Rust Book's classic first project.

🟡

3. Word frequency analyser

Concepts: HashMap, file I/O, iterators, sorting, error propagation with ?.

🟡

4. Markdown → HTML converter

Concepts: string slicing, enum state machine, pattern matching, String vs &str.

🟡

5. JSON parser from scratch

Concepts: recursive enums, Box, custom error types, iterators. The best single exercise in this list.

🟠

6. Multi-threaded file indexer

Concepts: thread::spawn, channels, Arc<Mutex>, WalkDir, error aggregation.

🟠

7. Redis clone (mini-redis)

Concepts: tokio, async, TCP sockets, protocol parsing, connection state. Follow the tokio tutorial.

🔴

8. Your own ls/grep/jq

Concepts: clap, cross-platform APIs, serde, release profiles, publishing binaries.

🔴

9. A small interpreter (Lox/Brainfuck)

Concepts: trees, Rc<RefCell>, trait objects, visitor patterns, lifetimes.

🔴

10. WebAssembly game or tool

Concepts: wasm-bindgen, web-sys, JavaScript interop, size optimisation.

Best learning resources

ResourceBest forStyle
The Rust Programming LanguageEveryone — the canonical textFree book, read cover to cover
RustlingsDrilling syntax & compiler errorsDozens of tiny exercises
Rust by ExampleLooking up "how do I…?"Code-first snippets
Comprehensive Rust (Google)Structured classroom-style courseSlides + exercises
Rust Language Cheat Sheet / PracticeIntermediate exercisesProblem sets by topic
Exercism Rust trackGetting mentored feedbackFree, human mentors
docs.rsLearning any crate's APIAuto-generated, with examples
Advent of CodePractising under light pressureGreat fit for Rust's strengths
Zero To Production In Rust (book)Building real web backendsPaid, project-based, excellent
Rust for Rustaceans (Jon Gjengset)Going from intermediate to advancedPaid, deep, opinionated

Habits that make the borrow checker your friend

  • Design the ownership first. Before writing code, ask "who owns this data, and for how long?"
  • Return values instead of mutating arguments. It's simpler, more testable, and avoids most borrow conflicts.
  • Prefer &T and &[T] over taking ownership of T in functions.
  • Read the error message completely. The last two lines usually contain the fix.
  • Use cargo check in the inner loop — it's 3–5× faster than cargo build.
  • Keep functions small while learning: fewer live borrows means fewer conflicts.
  • Don't fight with clone() silently — fix the ownership design instead.
  • Let the compiler drive. Every error is teaching you the model. It gets easier at roughly the two-week mark.
Chapter 20

Cheat sheets & glossary

Keep this chapter open in a second tab. It's the condensed version of everything above — designed for quick lookup while you code.

Syntax quick reference

cheat_sheet.rs
// ─── VARIABLES ────────────────────────────────────────────────────────────
let x = 5;                      // immutable
let mut y = 5;                  // mutable
let z: f64 = 2.5;               // explicit type
const N: u32 = 100;             // compile-time constant
let shadow = "hi"; let shadow = shadow.len();   // shadowing (may change type)

// ─── TYPES ────────────────────────────────────────────────────────────────
i8 i16 i32 i64 i128 isize       // signed integers
u8 u16 u32 u64 u128 usize       // unsigned integers
f32 f64                         // floats
bool char                       // 1 byte / 4 bytes (Unicode scalar)
()                              // unit type (like void)
(i32, f64)                      // tuple
[i32; 4]                        // fixed array
&[i32]                          // slice
Vec<i32>                        // growable list
String  /  &str                 // owned string / string slice
HashMap<K, V>  HashSet<T>       // hash collections
Option<T>  Result<T, E>         // absence / fallibility
Box<T>  Rc<T>  Arc<T>           // smart pointers

// ─── FUNCTIONS ────────────────────────────────────────────────────────────
fn add(a: i32, b: i32) -> i32 { a + b }        // tail expression
fn nothing() { }                               // returns ()
fn never() -> ! { panic!() }                   // diverging
fn generic<T: Trait>(x: &T) -> &T { x }       // generics
|a, b| a + b                                    // closure

// ─── CONTROL FLOW ─────────────────────────────────────────────────────────
if cond { } else if cond2 { } else { }
let v = if cond { 1 } else { 2 };              // if is an expression
let v = loop { break 42; };                    // loop with a value
while cond { }
for i in 0..10 { }                             // exclusive range
for x in &collection { }                       // borrow iteration
'outer: for x in 0..5 { break 'outer; }        // labelled break

// ─── MATCHING ─────────────────────────────────────────────────────────────
match value {
    Pattern => expr,
    1 | 2 => expr,                             // alternation
    10..=20 => expr,                           // range
    x if x > 5 => expr,                        // guard
    _ => expr,                                 // catch-all (required)
}
if let Some(x) = opt { }                       // single-case match
while let Some(x) = iter.next() { }
let Some(x) = opt else { return; };            // let-else

// ─── DATA ─────────────────────────────────────────────────────────────────
struct User { name: String }                    // named fields
struct Point(f64, f64);                         // tuple struct
struct Marker;                                  // unit struct
enum Color { Red, Rgb(u8,u8,u8) }               // data-carrying enum
impl User {
    fn new() -> Self { … }                      // associated fn: User::new()
    fn name(&self) -> &str { &self.name }       // method: u.name()
}

// ─── TRAITS ───────────────────────────────────────────────────────────────
trait Greet { fn hi(&self) -> String; }         // required method
trait Greet { fn hi(&self) -> String { … } }    // default method
impl Greet for User { … }                       // implement
fn f<T: Greet + Clone>(x: &T) { … }            // bounds
fn f(x: &impl Greet) { … }                      // impl Trait
fn f(x: &dyn Greet) { … }                       // dynamic dispatch
Box<dyn Error>                                  // trait object

// ─── ERROR HANDLING ───────────────────────────────────────────────────────
fn f() -> Result<i32, Box<dyn Error>> {
    let n = may_fail()?;                        // propagate
    Ok(n)
}
opt.unwrap_or(0);  opt.expect("why");  opt.map(f);  opt.and_then(f);
res.unwrap_or_else(|e| 0);  res.map_err(f);  res.ok_or("err");

// ─── CONCURRENCY ──────────────────────────────────────────────────────────
thread::spawn(move || { … });                   // OS thread
let (tx, rx) = mpsc::channel();                 // channel
Arc::new(Mutex::new(v))                         // shared mutable state
async fn f() { g().await; }                     // async

// ─── MODULES ──────────────────────────────────────────────────────────────
mod foo;                                        // declares a module
use crate::foo::Bar;                            // import
pub use foo::Bar;                               // re-export
pub(crate) fn helper() { }                      // crate-visible

// ─── ATTRIBUTES ───────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
#[cfg(test)]
#[allow(dead_code)]
#[inline]
#[must_use]
#[repr(C)]
#[tokio::main]

Common methods worth memorising

TaskCode
String → number"42".parse::<i32>()?;
Number → stringn.to_string() · format!("{n}")
Read a file to stringstd::fs::read_to_string("f.txt")?
Write a filestd::fs::write("f.txt", contents)?
Command-line argsstd::env::args().skip(1).collect::<Vec<_>>()
Environment variablestd::env::var("HOME") → Result<String>
Current timestd::time::Instant::now() · elapsed()
Iterate a mapfor (k, v) in &map { }
Sort a vecv.sort() · v.sort_by_key(|x| x.len())
Filter + map + collectit.filter(..).map(..).collect::<Vec<_>>()
Sum / min / maxit.sum::<i32>() · it.min() · it.max()
Option → Resultopt.ok_or("error")?
Result → Optionres.ok() · res.err()
JSON (serde)serde_json::to_string(&value)?
HTTP requestreqwest::get(url).await?.text().await?
Parallel mapdata.par_iter().map(..).sum() (rayon)

Glossary A–Z

Associated function
A function defined in an impl block without self. Called as Type::function().
Borrow
A reference to a value that does not take ownership. Comes in shared (&T) and mutable (&mut T) flavours.
Borrow checker
The compiler component that enforces the borrowing rules and lifetime constraints at compile time.
Crate
A compilation unit — either a library (lib.rs) or a binary (main.rs).
Closure
An anonymous function that captures variables from its surrounding environment.
Coherence
The property that a trait can only be implemented in the crate that defines the trait or the type (the orphan rule).
Copy
A marker trait meaning assignment duplicates the value instead of moving it. Only for stack-only types.
Drop
The trait whose drop(&mut self) runs automatically when a value goes out of scope — Rust's deterministic destructors.
Dyn
Keyword for dynamic dispatch: &dyn Trait uses a vtable at runtime instead of compile-time monomorphisation.
Edition
An opt-in language version (2015, 2018, 2021, 2024) that allows breaking changes without breaking old crates.
Elision
Lifetime elision: the compiler's rules for inferring lifetimes automatically so you rarely write them.
Expression
Anything that evaluates to a value. Blocks, if and match are all expressions in Rust.
FFI
Foreign Function Interface — calling or being called by C code via extern "C".
Monomorphisation
Compile-time specialisation of generic code for each concrete type used — the source of "free" generics.
Module
A namespace within a crate, controlling scoping and item visibility.
Newtype
A single-field tuple struct used to give a primitive type a distinct identity and stronger guarantees.
Ownership
The rule set determining that every value has exactly one owner and is dropped when the owner leaves scope.
Panic
An unrecoverable error. Unwinds the stack (or aborts) and exits the thread.
RAII
Resource Acquisition Is Initialisation: resources are bound to object lifetimes and released in Drop.
Raw pointer
*const T / *mut T. Unchecked, may be null or dangling — dereferencing requires unsafe.
Result
The enum Result<T, E> — either Ok(T) or Err(E). Rust's primary error-handling mechanism.
Slice
A borrowed view (pointer, length) into a contiguous sequence: &[T] or &str.
Shadowing
Declaring a new variable with the same name as a previous one, which is then inaccessible.
Static dispatch
Method calls resolved at compile time via monomorphisation. The default and the fastest.
Trait
A collection of methods defining shared behaviour — similar to an interface in other languages.
Turbofish
Explicit generic syntax at a call site: collect::<Vec<i32>>(). The ::<> shape.
Unsafe
A keyword granting five extra abilities while keeping the borrow checker. Small, explicit, greppable.
Zero-cost abstraction
High-level features that compile to the same code as hand-written low-level code.
Chapter 21

Test yourself

Answer each question in your head first, then open the answer. Getting one wrong is more valuable than getting it right — it points at exactly what to review.

1. Why does let s2 = s1; invalidate s1 when s1 is a String, but not when it's an i32?

String owns a heap allocation and does not implement Copy, so assignment moves ownership — otherwise both variables would free the same memory (double-free). i32 implements Copy: it lives entirely on the stack and duplication is trivial, so assignment makes a bitwise copy and both stay valid.

2. What are the three rules of ownership?

1. Every value has exactly one owner. 2. There can only be one owner at a time. 3. When the owner goes out of scope, the value is dropped (freed).

3. You have &v[0] and then call v.push(1) — the compiler refuses. Why?

push may reallocate the Vec's heap buffer to make room, which would invalidate the reference to the first element. The borrow checker is preventing a dangling pointer. Copy the value out, or end the borrow before mutating.

4. What is the difference between &T and &mut T at the rule level?

At any moment you may have either exactly one &mut T, or any number of &T — never both. This is what makes data races impossible in safe Rust.

5. Why does Rust have no null, and what replaces it?

Null references allow dereferencing nothing at runtime (the "billion-dollar mistake"). Rust replaces it with Option<T>, an enum with Some(T) and None. The compiler forces you to handle the None case before you can access the value.

6. What does the ? operator actually do?

On Ok(v)/Some(v) it unwraps to v. On Err(e)/None it returns early from the enclosing function, converting the error via From if needed. It's shorthand for a match with an early return.

7. When would you use dyn Trait instead of generics?

When you need a runtime choice of type: a Vec holding different implementations of a trait, a plugin system, or when you want to reduce compile time and binary size. Otherwise static dispatch (generics) is faster and the default.

8. What is the difference between String and &str?

String is owned, heap-allocated and growable. &str is a borrowed slice (pointer + length) into UTF-8 bytes — no allocation, no ownership. Function parameters should generally take &str for flexibility.

9. Why can't you index a String with s[0]?

Because a byte index may fall in the middle of a multi-byte UTF-8 character, producing invalid UTF-8 — a safety invariant of String. Use s.chars(), s.bytes(), or slice on char boundaries.

10. What is the difference between Rc<T> and Arc<T>?

Both provide shared ownership via reference counting. Rc uses non-atomic counters and is single-threaded only (!Send). Arc uses atomic operations, is thread-safe, and is very slightly slower.

11. What does "fearless concurrency" actually mean technically?

Because data can only be mutated when there is exactly one mutable reference, and references must be Send/Sync to cross thread boundaries, the compiler rejects data races at compile time. Concurrency bugs that are runtime errors elsewhere are compile errors in Rust.

12. What are monomorphisation's costs and benefits?

Benefit: generic code is specialised per type and inlined, giving zero-overhead abstractions and peak runtime performance. Cost: longer compile times and larger binaries, since one copy of the code is generated per concrete type used.

13. When should you reach for unsafe?

Almost never, and never to silence the borrow checker. Use it for FFI, raw pointer manipulation, intrusive data structures, or performance-critical internals — always wrapped in a small, well-tested, documented safe API.

14. In what area does Rust clearly lose to Go and C++?

Compile speed (especially clean builds) and learning curve. Go also wins on hiring pool and sheer simplicity of goroutines for typical network services. Rust's advantages are runtime performance, memory footprint and correctness guarantees.

🎓 If you answered 10+ correctly

You understand Rust's mental model. Go build something real — the syntax will follow naturally. If you got fewer than 8, re-read Chapter 05 and Chapter 10 — those two concepts explain almost everything else in the language.

The single most important thing to remember: the borrow checker is not your enemy. It is the compiler telling you, right now, about the bug that would have woken you up at 3am in production.