Skip to main content
  1. Posts/

Rust basics: ownership and borrowing

·637 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Ownership is the idea that makes Rust feel different from most mainstream languages. If this part stays fuzzy, the rest of the language keeps feeling arbitrary.

The short version is simple:

  • every value has an owner
  • there is only one owner at a time
  • when the owner goes out of scope, the value is dropped

Everything else is built on top of that.

Ownership in one example
#

let s = String::from("hello");

Here, s owns the String. When s goes out of scope, Rust automatically frees that heap-allocated memory.

This is how Rust avoids a garbage collector while still keeping memory management safe.

Moves happen by default for owned values
#

Some types are moved when assigned:

let a = String::from("hello");
let b = a;

// println!("{a}"); // invalid: a was moved
println!("{b}");

After let b = a;, ownership moved from a to b.

This prevents double-free bugs. If both bindings thought they owned the same String, both would try to clean it up.

Copy types behave differently
#

Small stack-only types like integers are copied instead of moved:

let x = 10;
let y = x;

println!("{x}");
println!("{y}");

That works because i32 implements Copy.

As a beginner rule:

  • integers, booleans, and chars are usually cheap copies
  • String, Vec<T>, and most heap-owning types move unless you explicitly clone them

Borrowing lets you use a value without taking ownership
#

Passing ownership everywhere would be awkward, so Rust lets you borrow:

fn print_length(s: &String) {
    println!("{}", s.len());
}

let name = String::from("m58");
print_length(&name);
println!("{name}");

The function receives a reference, not ownership, so name remains valid afterward.

Mutable borrowing
#

If a function should modify a value in place, borrow mutably:

fn append_world(s: &mut String) {
    s.push_str(" world");
}

let mut text = String::from("hello");
append_world(&mut text);

This is safe because Rust enforces exclusivity for mutable borrows.

Borrowing rules
#

The borrow checker enforces a small set of rules:

  • you can have any number of immutable references
  • or exactly one mutable reference
  • references must never outlive the value they point to

That is the core safety story.

For example, this is invalid:

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

let r1 = &s;
let r2 = &s;
// let r3 = &mut s; // invalid while r1 and r2 exist

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

Rust prevents mixing shared reads and exclusive mutation at the same time.

Slices are borrowed views
#

Borrowing is not just &String or &Vec<T>. Slices are borrowed views into part of a value:

let text = String::from("hello");
let first = &text[0..2];

println!("{first}");

For strings and vectors, slice types come up constantly:

  • &str is a borrowed string slice
  • &[T] is a borrowed slice of elements

That is why idiomatic Rust APIs often prefer:

fn greet(name: &str) { /* ... */ }
fn sum(values: &[i32]) { /* ... */ }

These signatures are more flexible than taking String or Vec<i32> directly.

Cloning is explicit
#

If you really need a second owned copy, call clone():

let a = String::from("hello");
let b = a.clone();

println!("{a}");
println!("{b}");

Rust makes cloning explicit on purpose. Hidden deep copies make performance harder to reason about.

Why this model is worth the trouble
#

Ownership and borrowing buy Rust three things at once:

  • memory safety without a garbage collector
  • clear data flow at function boundaries
  • fewer accidental aliasing and lifetime bugs

That is the real reason the compiler feels strict. It is enforcing rules that many other languages leave to runtime bugs or discipline.

Summary
#

  • owned values move by default
  • borrowed values let you read or mutate without taking ownership
  • Copy types are duplicated implicitly, but heap-owning types usually are not
  • the borrow checker prevents invalid sharing and mutation patterns

If you are learning Rust, treat ownership as the main concept, not an advanced one. Most of the language becomes more coherent once this part is solid.

Related

Rust basics: Option and Result

·481 words·3 mins
Rust does not use null for ordinary absence, and it does not rely on exceptions for recoverable errors. Instead, it uses enums: Option<T> and Result<T, E>. These two types show up everywhere. If you understand them early, a lot of Rust APIs stop looking strange.

Rust: & vs * (references and dereferencing)

·569 words·3 mins
& borrows a value. * follows a reference to reach the value behind it. The syntax is small, but it sits right in the middle of Rust’s ownership model. If references feel awkward at first, that is normal. In Rust they are not just pointers with nicer spelling. They are part of the language’s safety model.