References & borrowing
How to use a value without owning it. &T is a shared, read-only
reference — there can be many. &mut T is an exclusive, read-write
reference — there can be exactly one. Never both at once. This is the
aliasing-XOR-mutation rule and the foundation of Rust's safety.
1 · The intuition
Ownership says only one variable owns a value at a time. Borrowing lets other
code use the value without taking ownership. The compiler enforces two rules:
(a) at any one moment, you can have many &T (shared) or
one &mut T (exclusive), never both; (b) references must not
outlive the value they point at.
2 · Shared references — read many
fn print_len(s: &String) {
println!("len = {}", s.len());
// s is a shared reference — we can read but not mutate
}
fn main() {
let owned = String::from("hello");
print_len(&owned);
print_len(&owned); // call again — still owned by main
println!("{}", owned); // still ours
}3 · Mutable references — exactly one
fn append_bang(s: &mut String) {
s.push('!');
}
fn main() {
let mut owned = String::from("hello");
append_bang(&mut owned);
append_bang(&mut owned);
println!("{}", owned);
}&mut requires the underlying variable to be declared
mut. The function declares it takes a &mut String.
The caller passes &mut owned. Three places that have to agree —
and the compiler enforces all three.
4 · The famous rule — aliasing XOR mutation
fn main() {
let mut s = String::from("hello");
let r1 = &s; // shared borrow
let r2 = &s; // another shared — fine
let r3 = &mut s; // ERROR: cannot borrow s as mutable because it is also borrowed as immutable
println!("{} {} {}", r1, r2, r3);
}You may have many shared references at the same time. Or one mutable reference. You may not have both. This is the rule that catches every data race at compile time — and the rule that the first month of Rust feels like a fight with the compiler about.
let r1 = &s; println!("{}", r1); let r2 = &mut s;
compiles — r1 isn't used after the print.5 · Common patterns
// Read-only — take &T
fn sum(xs: &Vec<i32>) -> i32 {
xs.iter().sum()
}
// Modify in place — take &mut T
fn push_double(xs: &mut Vec<i32>, v: i32) {
xs.push(v * 2);
}
fn main() {
let mut xs = vec![1, 2, 3];
println!("sum = {}", sum(&xs));
push_double(&mut xs, 5);
println!("xs = {:?}", xs);
println!("sum = {}", sum(&xs));
}6 · Common mistakes
- Borrowing mutably while still using a shared borrow. The compiler is precise about overlap; restructure so they don't overlap, or drop one explicitly.
- Mixing
&Tand&mut Ton the same struct field. Sometimes the fix is splitting into two structs that the compiler can borrow independently. - Returning a reference to a local. Won't compile — lifetimes (next concept). Return owned or take a reference to extend the caller's data.
- Forgetting that
selfis a borrow. Methods take&self(read),&mut self(write), orself(consume). The same rules apply.
7 · When it clicks
- Function signatures tell you the access pattern at a glance.
- You reach for
&Tby default,&mut Tonly when you need to mutate. - The "cannot borrow as mutable while immutable borrow is in use" error becomes a useful hint rather than a frustration.
- You stop reaching for
.clone()as a borrow-checker fix.