Rust for Gophers
Memory Ownership Fundamentals
Ownership: Rust's Memory Management
In Go, you're used to a garbage collector (GC) that cleans up memory for you. You pass pointers around, and the GC figures out when an object is no longer needed. Rust takes a completely different approach. It has no GC. Instead, it enforces a set of rules at compile time called the ownership system. This system guarantees memory safety without the runtime cost of a garbage collector.
The core rules of ownership are:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped.
These three rules are the foundation of Rust's safety and performance guarantees. Let's break down how they work in practice.
The Stack and the Heap
Like many systems languages, Rust manages memory in two places: the stack and the heap. The stack is for data with a known, fixed size. It's fast because it's a simple last-in, first-out structure. When you call a function, its local variables and a pointer to where to return are pushed onto the stack. When the function finishes, they're popped off.
The heap is for data whose size might change at runtime, like a user-provided string. When you need space on the heap, you ask the memory allocator for a certain amount, and it gives you a pointer to that location. This is slower than the stack. Ownership is primarily about managing heap data, ensuring there's exactly one free for every allocate.
Move Semantics
Here's the biggest shift from Go. Consider this code handling a String, a type that stores its data on the heap.
let s1 = String::from("hello");
let s2 = s1;
// This next line will cause a compile error!
// println!("{}", s1);
In Go, if s1 were a pointer, s2 would become another pointer to the same data. In Rust, this is a move. The ownership of the String data is transferred from s1 to s2. After the move, s1 is no longer valid. The compiler will prevent you from using it, which eliminates the possibility of a "double free" error, where two variables might try to free the same memory when they go out of scope.
The same logic applies when passing values to functions. Passing a heap-allocated variable to a function moves ownership to the function's parameter. The original variable can no longer be used unless the function explicitly returns ownership.
fn main() {
let s = String::from("takes"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move, but i32 is Copy, so it’s okay
// to still use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
Ownership is Rust's innovative approach to memory management, ensuring memory safety without garbage collection.
The Copy and Clone Traits
So why did the integer x in the example above still work after being passed to a function? Because simple scalar types like integers, floats, booleans, and characters don't need to be allocated on the heap. They have a known, fixed size and are stored entirely on the stack.
Types like this implement the Copy trait. When a Copy type is assigned to another variable, a bit-for-bit copy of the value is made. There's no pointer to manage, so the original variable remains valid. This is why you can assign integers freely without worrying about ownership moves.
Finally, when a variable that owns heap data goes out of scope (for instance, at the end of a function), Rust automatically calls a special function called drop. This function is where the code to deallocate the memory lives. Because ownership is unique, Rust knows exactly when to call drop, making memory deallocation deterministic and predictable, unlike a GC which might pause your program at any time.
What is the primary purpose of Rust's ownership system?
In Rust, simple scalar types like integers and booleans are stored on the stack and implement the Copy trait.
This system of ownership, moving, and dropping is how Rust achieves memory safety at compile time. It might feel restrictive at first, but it eliminates a whole class of bugs common in other systems languages.
