Introduction
Moving ownership every time you call a function would be incredibly tedious. Borrowing lets you give a function temporary access to a value without transferring ownership. This is one of Rust's most frequently used mechanisms and is essential for writing ergonomic, safe code.
Key Concepts
- Reference (
&T): An immutable borrow that lets you read a value without owning it. The original owner retains ownership. - Mutable reference (
&mut T): A borrow that lets you both read and modify the value. Only one mutable reference can exist at a time. - Borrow checker: The compiler component that enforces borrowing rules at compile time, preventing data races and dangling references.
Real World Context
Every Rust function that reads data without consuming it uses references. In a web server, for example, request handlers borrow the shared application state rather than cloning it for each request. The borrow checker guarantees that no two handlers can mutate the same state simultaneously, eliminating an entire class of concurrency bugs.
Deep Dive
You create a reference with the & operator. The function receives access to the data without taking ownership:
rustfn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); println!("'{}' has length {}.", s1, len); // s1 is still valid
The borrow checker enforces two rules. At any given time, you can have either one mutable reference or any number of immutable references, but never both simultaneously:
rustlet mut s = String::from("hello"); let r1 = &s; // OK — immutable borrow let r2 = &s; // OK — multiple immutable borrows allowed println!("{} and {}", r1, r2); // r1 and r2 are no longer used after this point let r3 = &mut s; // OK — no active immutable borrows r3.push_str(", world");
Mutable references let you modify borrowed data:
rustfn append_greeting(s: &mut String) { s.push_str(", world"); } let mut s = String::from("hello"); append_greeting(&mut s); println!("{}", s); // "hello, world"
Common Pitfalls
- Mixing mutable and immutable borrows — You cannot hold an immutable reference and a mutable reference to the same data at the same time. Move the immutable usage above the mutable borrow so the compiler sees they do not overlap.
- Forgetting
muton the variable — To create a&mutreference, the underlying variable must be declared withlet mut. A common error is trying to mutably borrow an immutable binding.
Best Practices
- Accept
&strinstead of&Stringin function parameters —&stris more flexible because it accepts bothStringreferences and string literals without extra conversion. - Keep borrow scopes as short as possible — The shorter the lifetime of a reference, the less likely you are to run into borrow checker conflicts.
Summary
- References (
&T) let you borrow data without taking ownership. - Mutable references (
&mut T) allow modification but enforce exclusive access. - You may have many immutable references or one mutable reference, never both at once.
- The borrow checker enforces these rules at compile time, preventing data races.
- Prefer
&strover&Stringfor function parameters to maximize flexibility.
Code Examples
fn main() {
let mut v = vec![1, 2, 3];
// This would be a data race in C++!
// let first = &v[0]; // Immutable borrow
// v.push(4); // Mutable borrow - Error!
// println!("{}", first);
// Correct approach:
v.push(4);
let first = &v[0];
println!("First: {}", first);
}