Rust Borrowing and References Explained: The Key to Memory Safety Without GC

Rust enforces memory safety at compile time through borrowing rules. Without relying on a garbage collector, it prevents data races, dangling references, and illegal mutation. This article focuses on three core mechanisms—references, mutable borrowing, and dangling references—to help you understand how Rust’s ownership model supports high-performance systems programming. Keywords: Rust, borrowing, memory safety.

Technical Specification Snapshot

Parameter Description
Language Rust
Core Topics Ownership, Borrowing, References, Dangling References
Memory Model Compile-time ownership checks, no garbage collection
Typical Risks Data races, dangling pointers, illegal mutable access
Star Count Not provided in the source
Core Dependencies Rust standard library String, borrow checker
Applicable Scenarios Systems programming, performance-sensitive services, concurrent development

Rust Reuses Data Ownership Through Borrowing Instead of GC

Rust’s key design is not “automatic reclamation,” but “restricting who can access data, when they can access it, and whether they can modify it.” Every value always has a clear owner, and borrowing allows other code to access that value temporarily without taking ownership.

This resolves a common tradeoff in traditional languages: either rely on GC and pay runtime overhead, or manage memory manually and risk dangling pointers and double frees. Rust chooses a third path by moving correctness checks into compile time.

References Let Functions Read Data Without Transferring Ownership

If you pass a String directly to a function, ownership moves. If the function then needs to return the value, the interface becomes awkward.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // Borrow s1 instead of transferring ownership
    println!("The length of '{}' is {}.", s1, len); // s1 is still usable
}

fn calculate_length(s: &String) -> usize {
    s.len() // Read-only access to the string length
}

This example demonstrates an immutable borrow: the function can read the data, but it cannot take ownership or modify the content.

&s1 creates a reference, not a copy and not an owner. When the reference goes out of scope, it does not free the underlying data because it never owned the data.

Borrowing Fundamentally Grants Access, Not Ownership

You can think of borrowing as “temporary viewing permission.” The borrowed value is still released by the original variable, so &String in a function signature is a strong constraint: read is allowed, mutation is not.

fn main() {
    let s = String::from("hello");
    change(&s); // Pass an immutable reference
}

fn change(some_string: &String) {
    some_string.push_str(", world"); // Compile error: cannot modify through an immutable reference
}

This code fails immediately because references are immutable by default. Rust uses this rule to prevent the hidden risk of “writing while shared for reading.”

Error when modifying through an immutable reference AI Visual Insight: This screenshot shows the borrow-related error emitted by the Rust compiler at the push_str call. The core message is that the function receives an immutable reference, &String, so it cannot perform a write operation that changes the contents of the underlying buffer. Errors like this usually include precise line numbers, type information, and fix suggestions, reflecting the borrow checker’s strict distinction between read-only and writable borrows.

Mutable Borrowing Prevents Concurrent Write Conflicts Through Exclusivity

When you do need to modify data, Rust allows mutable references—but only when access is exclusive. In other words, only one mutable borrow may exist at a time.

fn main() {
    let mut s = String::from("hello");
    change(&mut s); // Create a mutable borrow
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world"); // Modify content through a mutable reference
}

This code safely modifies the original string, as long as both s and its borrow are explicitly marked mutable.

Rust Forbids Multiple Mutable References at the Same Time

The following code may look ordinary, but it violates Rust’s exclusive borrowing rule.

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

    let r1 = &mut s; // First mutable borrow
    let r2 = &mut s; // Compile error: cannot create a second mutable borrow at the same time

    println!("{}, {}", r1, r2);
}

Rust rejects this code because two mutable references could modify the same memory simultaneously, leading to a data race.

Error when creating multiple mutable references to a specific variable AI Visual Insight: This compiler error screenshot reflects the borrow checker’s static rejection of creating repeated &mut references within the same scope. The compiler points out where the first mutable borrow occurs, where the second borrow occurs, and where the first borrow is later used, making it clear why the lifetimes of the two writable references overlap.

This Restriction Eliminates Data Races at Compile Time

A data race typically requires three conditions: multiple pointers access the same data, at least one performs a write, and there is no synchronization mechanism. Rust prevents these conditions from being satisfied at the same time through its type system and scope rules.

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

    {
        let r1 = &mut s; // Restrict r1 to the inner scope only
        r1.push_str(" rust");
    } // r1 ends here

    let r2 = &mut s; // After the previous borrow ends, borrowing is allowed again
    r2.push_str("!");
}

This example shows that Rust does not forbid repeated mutable borrowing. It forbids overlapping mutable borrows.

Rust Prevents Dangling References During Compilation

A dangling reference points to memory that has already been freed. In C and C++, this kind of bug is often deeply hidden and may not surface until runtime.

Rust’s strategy is straightforward: if a reference might outlive the data it points to, the code does not compile.

fn main() {
    let reference_to_nothing = dangle();
    println!("{}", reference_to_nothing);
}

fn dangle() -> String {
    let s = String::from("hello");
    s // Return ownership instead of returning a reference to a local variable
}

This example shows the correct fix: return ownership of the String instead of returning a reference to a local variable.

If you wrote the function to return &String, the local variable s would be dropped when the function ends, and the reference would become invalid. Rust’s borrow checker stops this behavior at compile time.

Dangling pointer compile error AI Visual Insight: This image shows a typical dangling reference compile error: the function tries to return a reference to the local variable s, but s will be destroyed when the function exits. The compiler flags the illegal act of returning a reference to a local variable and emphasizes that the return value’s lifetime exceeds the actual lifetime of the referenced object.

Borrowing Rules Form Rust’s Minimal Closed Loop for Memory Safety

From read-only references, to exclusive mutable references, to the prevention of dangling references, Rust builds a tightly coordinated static safety model. Developers may appear to write a few extra &, &mut, and scope boundaries, but in return they get deterministic memory behavior.

This is also the fundamental reason Rust continues to gain adoption in systems programming: no GC pauses, no manual deallocation burden, and many memory errors eliminated before the program ever runs.

FAQ

What is the relationship between borrowing and ownership in Rust?

Borrowing is the access mechanism within Rust’s ownership system. A value always has exactly one owner, but code can temporarily borrow read or write access without transferring ownership.

Why are multiple immutable references allowed, but multiple mutable references are not?

Multiple immutable references only read data and do not break consistency. Multiple mutable references could write to the same data at the same time and cause data races, so Rust forbids them at compile time.

How can Rust guarantee memory safety without GC?

Rust ties resource cleanup to scope and delegates access validation to the borrow checker. This avoids runtime garbage collection overhead while also preventing dangling references, double frees, and concurrent write conflicts.

AI Readability Summary

This article systematically reconstructs the core concepts behind Rust’s borrowing mechanism. It focuses on three major topics—references, mutable borrowing, and dangling references—and explains how Rust uses compile-time rules to prevent data races and illegal memory access without relying on garbage collection.