Understanding References and Borrowing in Rust

Borrow a String through shared or mutable references and understand when each is valid. The examples connect reference syntax, dereferencing, lifetimes, and data-race prevention.

Introduction to References and Borrowing

Rust references let a function use data without taking ownership of it. A shared reference uses &T; a mutable reference uses &mut T. Start with Rust ownership if moves are new to you, then follow these examples of borrowing a String.

Using References Instead of Moving Values

Passing an owned, non-Copy value such as String by value moves it into the function. Passing a reference lets the function access the original value without taking ownership. Values of Copy types, such as integers, are copied instead.

Consider the following function:

fn do_something(s: &String) {
    println!("{}", s);
}
Rust

Here, the & symbol is used to create a reference to the String rather than moving the String itself. The reference points to the value without taking ownership of it. This means the original variable retains ownership, and the function merely borrows it for a while.

let s1 = String::from("abc");
do_something(&s1);
Rust

In this example, s1 remains valid because do_something receives a shared reference rather than taking ownership. A borrow only needs to remain active while the reference is used; it does not necessarily last until the end of the surrounding lexical scope.

Here’s how this looks:

s (reference)         s1 (owner)
|-----|---|         |-----|---|
| ptr |   | ------> | ptr |   |
|-----|---|         | len | 3 |
                    | cap | 3 |
                    |-----|---|
Plaintext

What’s Happening Under the Hood?

When you create a reference to s1, Rust internally creates a pointer to the value s1. This pointer allows the function to access the value without owning it. Rust automatically manages the creation and destruction of these pointers, ensuring they are always valid.

One of Rust’s most powerful features is its ability to enforce lifetimes, ensuring that references do not outlive the data they point to. This avoids common bugs in other languages, such as dangling pointers or null references.

Mutable References

A shared reference, &T, does not allow ordinary mutation of the referenced value. A mutable reference, &mut T, provides exclusive access that permits mutation. This is separate from whether the variable holding the reference can be reassigned.

fn do_something(s: &mut String) {
    s.push_str("def");
}

let mut s1 = String::from("abc");
do_something(&mut s1);
println!("{}", s1);
Rust

The call passes a mutable reference to s1 into do_something. The function can modify the string through that reference. The following diagram shows the relationship:

s (mutable reference)  s1 (owner)
|-----|---|         |-----|---|
| ptr |   | ------> | ptr |   |
|-----|---|         | len | 3 |
                    | cap | 3 |
                    |-----|---|
Plaintext

The Dot Operator and Dereferencing

Rust has a unique feature with the dot operator (.). When you use the dot operator for method calls or accessing fields, Rust automatically dereferences down to the actual value, so you don’t need to worry whether you’re dealing with a value or a reference.

However, if you need to manually dereference a reference, you can use the * operator:

fn do_something(s: &mut String) {
    (*s).push_str("def");
}
Rust

The dereference operator (*) has lower precedence than the dot operator. This means you sometimes need to use parentheses to clarify your intentions. For example:

fn do_something(s: &mut String) {
    *s = String::from("def");
}
Rust

The assignment dereferences s and replaces the borrowed String with a new value.

Rust’s Rules on References

For a given value, you can have either one active mutable borrow or multiple active shared borrows. The relevant period is when the references are used. This prevents conflicting access to that value; the Rust Book explains the rules and reference lifetimes.

References in Multithreaded Contexts

Let’s consider a scenario where two threads are borrowing references to the same variable:

Thread A
  ref1
    |----|--|
    | ptr|  |
    |----|--|  ---------->    Thread C
                                  s1
Thread B       -------->  |----|----|     |-----|
  ref2                    | ptr |   | --> | a   |
    |----|--|             | len | 3 |     | b   |
    | ptr|  |             | cap | 3 |     | c   |
    |----|--|             |----|----|     |-----|
Plaintext

In a multithreaded environment, having multiple mutable references to the same variable can lead to unpredictable behavior, data corruption, or crashes. Rust’s borrowing rules prevent this by ensuring that only one mutable reference exists at any time, or multiple immutable references exist simultaneously but without any mutable reference.

Conclusion

Borrowing separates access from ownership. Use a shared reference for reading and a mutable reference when the function needs exclusive access to modify a value. Revisit String and &str to apply these choices to text parameters, or use the Rust Fundamentals series to review earlier lessons.

Share
Prabhat Kashyap

Prabhat Kashyap

Senior Technical Architect at HCLTech · working with Leonteq Security AG

I have 10+ years of experience building distributed systems and fintech platforms. I write about practical, non-obvious engineering details that official documentation often skips.

Newsletter

Get new engineering deep dives

Scala, JVM, AI, and backend systems. I send practical articles when there is something worth reading.

Join the newsletter