Borrowing & References

Course 1 · Ch 4
Borrowing & References
Using a value without taking ownership of it — and completing the answer to "how does Rust replace a GC?"

Chapter 3 ended with a real frustration: passing a value to a function moves it, leaving the caller unable to use it afterward. Borrowing is the fix — letting a function use a value without taking ownership of it at all.

References With &

fn calculate_length(s: &String) -> usize { s.len() } fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("{} is {} bytes long", s1, len); // s1 is still valid! }

&s1 creates a reference to s1 rather than moving it — calculate_length can read the string, but never owns it. Once the function returns, s1 is exactly as valid as before, solving Chapter 3's function-ownership problem directly.

Mutable References With &mut

fn add_exclamation(s: &mut String) { s.push_str("!"); } fn main() { let mut s = String::from("hello"); add_exclamation(&mut s); println!("{}", s); // hello! }

&mut lets a function genuinely modify the borrowed value — but this power comes with a strict rule.

The Borrow Checker's Rules

  1. At any given time, you can have either any number of immutable references (&) or exactly one mutable reference (&mut) — never both at once.
  2. References must always be valid — no reference to something that's already been dropped.

Both rules are enforced entirely at compile time.

SituationAllowed?
Two immutable references (&s, &s) at onceYes
One mutable reference (&mut s) aloneYes
One mutable + one immutable reference, both active at onceNo — compile error
Two mutable references, both active at onceNo — compile error

Why These Rules Exist

The mutable-XOR-immutable rule prevents a real, classic bug class: something reading data while something else modifies it underneath it. Concretely — if two mutable references to a growing collection existed simultaneously, one holder could trigger a reallocation (the collection outgrowing its current memory and moving elsewhere) while the other still points at the old, now-freed memory location — a genuine dangling-pointer bug that has caused real, exploitable vulnerabilities in C++. Rust's borrow checker makes this class of bug simply not compile, rather than something to carefully avoid at runtime. This is also the foundation of Rust's "fearless concurrency" (Course 2) — the exact same rule that prevents this single-threaded aliasing bug also prevents data races between threads.

Dangling References

fn dangle() -> &String { let s = String::from("hello"); &s // compile error: `s` does not live long enough } // s is dropped here — the reference would point at freed memory

In C or C++, this same pattern compiles and produces a genuine dangling pointer — undefined behavior waiting to happen at runtime, possibly much later and far from where the actual mistake was made. In Rust, it's simply a compile error: the borrow checker sees that s is dropped at the end of the function, and refuses to let a reference to it escape.

Rust's Alternative to Go's GC

The full arc from Chapter 1 is now complete. Go's garbage collector scans memory at runtime to find and free data nothing references anymore — genuinely useful, but with real runtime overhead and pause times that are hard to predict exactly when they'll land. Rust's borrow checker instead analyzes ownership and borrowing entirely at compile time — by the time a Rust program actually runs, every memory-safety guarantee has already been proven, with zero runtime cost for that safety, no background process ever running, and the deterministic drop timing Chapter 3 introduced.

RuleWhat It Prevents
One owner at a time (Ch.3)Ambiguity about who's responsible for freeing memory
Mutable XOR immutable referencesReading data while it's being modified (aliasing bugs, data races)
References must stay validDangling references / use-after-free
The complete picture: Chapters 1 → 4
Chapter 1 named the goal (memory safety, no GC). Chapter 3 supplied the ownership rules that make cleanup deterministic. This chapter supplies the borrow checker, which lets code actually use data without constantly transferring ownership back and forth — while still catching, at compile time, every aliasing bug a garbage collector was never designed to catch in the first place.
A reference's "scope" ends at its last use, not the end of the block
Modern Rust uses non-lexical lifetimes — a reference is considered to have ended as soon as it's last actually used, not necessarily at the closing brace of its enclosing block. This means two references can sometimes coexist in code that looks like it should conflict, as long as the first one's last use happens before the second one is created. This is a genuine subtlety that trips up anyone relying on an older mental model (or an outdated tutorial) — Course 2's dedicated Lifetimes chapter covers the full picture; for now, just know the borrow checker is often smarter about "how long a reference lasts" than a first glance at the code might suggest.

Coding Challenges

Challenge 1

Write a function count_words that takes a &String and returns the number of words (split_whitespace().count()) without taking ownership. Call it twice on the same variable to prove it remains valid after each call.

📄 View solution
Challenge 2

Write code that creates one mutable String, then attempts to hold both an immutable reference and a mutable reference to it at the same time (using both before the function ends). Run cargo run, note the exact compiler error, then fix it by ensuring the immutable reference's last use happens before the mutable one is created.

📄 View solution
Challenge 3

Explain, in your own words, why Rust's approach to memory safety has zero runtime cost compared to Go's garbage collector — referencing specifically when each language's safety mechanism actually does its work.

📄 View solution

Chapter 4 Quick Reference

  • &value — an immutable reference; the function can read but not modify, and never takes ownership
  • &mut value — a mutable reference; allows modification, but only one may exist at a time
  • Borrow rule: any number of & references, OR exactly one &mut reference — never both simultaneously
  • References must stay valid — the compiler rejects any reference that could outlive the data it points to
  • These rules prevent aliasing bugs and data races at compile time — the same foundation behind Rust's "fearless concurrency"
  • Rust vs. Go's GC: Rust proves memory safety at compile time (zero runtime cost); Go's GC checks at runtime (real, ongoing overhead)
  • Non-lexical lifetimes: a reference's effective scope ends at its last use, not the closing brace — full lifetime syntax is Course 2's territory
  • Next chapter: Structs & Methods — struct definitions, impl blocks, and methods vs. associated functions