#6 References and borrowing
&, &mut, borrowing rules
What is borrowing?
In the previous lesson, we saw that passing a value to a function moves ownership. Borrowing lets you use a value without taking ownership of it. You create a reference to the value instead:
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it doesn't own the data — nothing is dropped
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // Pass a reference
println!("Length of \"{s}\" is {len}"); // s is still valid!
}The & symbol creates a reference. The function borrows the value but does not own it. When the reference goes out of scope, the original value is unaffected.
Immutable references (&T)
By default, references are immutable. You can read the value but cannot modify it:
fn print_greeting(s: &String) {
println!("Hello, {s}!");
// s.push_str("!!!"); // ERROR: cannot modify through immutable reference
}
fn main() {
let name = String::from("Rust");
print_greeting(&name);
println!("s is still valid: {name}");
}You can have multiple immutable references at the same time — reading is always safe when no one is writing.
Mutable references (&mut T)
To modify a borrowed value, you need a mutable reference with &mut:
fn add_world(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
println!("Changed: {s}"); // "hello, world!"
}Notice that both the variable (let mut s) and the reference (&mut s) must be declared as mutable.
Borrowing rules
Rust enforces two critical rules at compile time to prevent data races:
// Rule 1: You can have EITHER:
// - Any number of immutable references (&T)
// - OR exactly one mutable reference (&mut T)
// ...but NOT both at the same time.
let mut s = String::from("hello");
let r1 = &s; // OK — immutable borrow
let r2 = &s; // OK — another immutable borrow
println!("{r1}, {r2}");
// r1 and r2 are no longer used after this point
let r3 = &mut s; // OK — mutable borrow (r1, r2 are done)
r3.push_str("!");
// Rule 2: References must always be valid
// (no dangling references — see below)Rust's borrow checker is smart: a reference's lifetime ends at its last use, not at the end of the scope. This is called Non-Lexical Lifetimes (NLL).
Dangling references
Rust prevents dangling references — references that point to freed memory. The compiler catches this at compile time:
fn dangle() -> &String {
let s = String::from("hello");
&s // ERROR: s is dropped at end of function!
} // s goes out of scope — reference would be invalid
// Fix: return the owned value instead
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved to the caller
}This guarantee is one of Rust's biggest strengths: if your code compiles, there are no dangling pointers or use-after-free bugs.
Your turn
Try running the references demo with cargo run and cargo check in the terminal below: