Memory management in Rust: ownership, borrowing, and lifetimes demystifiedCode & Development

Code & Development · 18 Jun 2026

Memory management in Rust: ownership, borrowing, and lifetimes demystified

C gives you total control—and a loaded gun. Go uses a Garbage Collector—and latency spikes. Rust ensures memory safety at compile time, zero cost.

Genildo Souza18 Jun 2026Read: 7 min

The problem that every language solves differently

Every language needs to answer a fundamental question: when can the memory a value occupied be freed? The answer defines everything — performance, security, predictability.

The Fundamental Memory Management Question

Every programming language must decide when memory can be freed, impacting performance, safety, and developer responsibility.

Manual management (C/C++) offers control but risks leaks, double frees, and use-after-free errors, causing many vulnerabilities.

Garbage collection (Go) automates freeing but causes unpredictable pauses, while Rust’s ownership model enforces safe, precise memory freeing at compile time without runtime costs.

C / C++ — manual control

You allocate and you free. Maximum performance, maximum responsibility. Forgot one? free(): memory leak. Freed twice: double-free. Accessed after freeing: use-after-free. 70% of Microsoft's vulnerabilities came from here.

Go — Garbage Collector

The runtime decides. You don't manage memory — the GC scans and frees what is no longer in use. Safe, but with a cost: unpredictable periodic pauses. Discord migrated because the GC caused spikes every 2 minutes.

Rust — Ownership

The compiler decides. Rules verified at compile time ensure that memory is freed exactly when it should be — no GC, no manual management, no runtime surprises.

Ownership: every value has an owner

Rust's ownership system has three simple rules. All of the language's memory safety derives from them.

1

Each value has exactly one owner

Uma variável é a dona do valor que ela armazena. Não existe valor sem dono.

2

There can only be one owner at a time

Quando você atribui um valor a outra variável, a propriedade se transfere — a variável original deixa de ser válida.

3

When the owner goes out of scope, the value is dropped

Sem GC, sem free(). O compilador insere a liberação automaticamente quando a variável deixa de existir.

In practice, it looks like this:

Code
let s1 = String::from("blueprint");
let s2 = s1; // ownership se transfere pra s2

println!("{}", s1); // ❌ erro de compilação: s1 não é mais válida
println!("{}", s2); // ✅ s2 é a nova dona

In C or Go, this code would work — and you would have two pointers pointing to the same data in memory. In Rust, the compiler refuses it immediately. There is no chance of a double-free or accidental use of a value that has already been moved.

Why String and not &str? Types that live on the stack (like integers) are copied automatically — they are cheap enough. Types that live on the heap (like String) transfer ownership. If you want to copy a String, you need to call .clone() explicitly — which makes the cost of the operation visible in the code.

Borrowing: using without being the owner

If ownership were the only way to access data, you would have to transfer ownership every time you passed something to a function — and receive it back later. Infeasible.

Ownership Transfer vs Borrowing in Rust Ownership transfer Borrowing (reference) Pass reference Variable A: owns String Owns the heap data Variable B: takes ownership Ownership transferred from A Reference to String Borrowed from Variable A Function receiving &String Uses reference without ownership
Ownership Transfer vs Borrowing in Rust

That is where borrowing comes in: you lend a reference to the value without transferring ownership.

Code
fn tamanho(s: &String) -> usize { // recebe uma referência, não o valor
    s.len()
}

let minha_string = String::from("blueprint");
let tam = tamanho(&minha_string); // passa a referência

println!("{} tem {} caracteres", minha_string, tam);
// ✅ minha_string ainda é válida — ownership não foi transferida

Mutable references — and the most important rule of borrowing

You can lend mutability too — but with a crucial restriction:

Code
let mut s = String::from("blueprint");

let r1 = &mut s;
let r2 = &mut s; // ❌ erro: não pode ter duas referências mutáveis ao mesmo tempo
Code
let mut s = String::from("blueprint");

{
    let r1 = &mut s;
    r1.push_str(" blog");
} // r1 sai de escopo aqui

let r2 = &mut s; // ✅ agora pode — r1 já não existe

This rule eliminates data races at compile time. In Go or C, two threads being able to write to the same data at the same time is a ticking time bomb. In Rust, the compiler simply doesn't let this happen.

Borrowing rules prevent data races at compile time

Rust enforces that you can have multiple immutable references or exactly one mutable reference to data at a time, never both simultaneously. This strict rule ensures that data races cannot occur during concurrent access.

Unlike languages like Go or C, where simultaneous writes by multiple threads can cause unpredictable bugs, Rust's compiler prevents such scenarios before the program even runs.

This compile-time guarantee is a key advantage of Rust, eliminating an entire class of concurrency errors by design.

The complete borrowing rule: you can have any number of immutable references or exactly one mutable reference — never both at the same time. This is enough to eliminate an entire category of concurrency bugs.

Lifetimes: when the compiler needs help

The Rust compiler can infer most ownership and borrowing situations automatically. But there is one case where it needs an explicit hint: when a function returns a reference, and the compiler needs to know which of the parameters it comes from.

Code
// Sem lifetime — o compilador não sabe de onde vem a referência retornada
fn maior(x: &str, y: &str) -> &str { // ❌ erro de compilação
    if x.len() > y.len() { x } else { y }
}
Code
// Com lifetime — o compilador sabe que o retorno vive tanto quanto x e y
fn maior<'a>(x: &'a str, y: &'a str) -> &'a str { // ✅
    if x.len() > y.len() { x } else { y }
}

The 'a is a lifetime annotation. It doesn't create anything — it just tells the compiler: "the returned reference lives for the same amount of time as both parameters." With this information, it can verify that you will never return a reference to something that has already been freed.

Lifetimes are the biggest friction point in Rust. The good news: the compiler needed explicit annotations in much simpler cases in the past. Modern versions of Rust infer lifetimes in most cases with the lifetime elision rules — you only need to annotate when the situation is genuinely ambiguous.

Comparing in practice: the same bug in three languages

Use-after-free is one of the most common and dangerous vulnerabilities. See how each language handles it:

C: Undefined Behavior

Memory is manually allocated and freed.

Use-after-free occurs when accessing freed memory.

Compiler does not catch the error.

Program compiles and runs but behavior is unpredictable.

Leads to potential security vulnerabilities.

Go: Runtime GC Prevention

Garbage collector manages memory automatically.

Prevents premature freeing of memory.

Use-after-free is avoided at runtime.

No compile-time errors related to memory use.

Adds runtime overhead due to GC management.

Code
/* C — compila, executa, comportamento indefinido */
char *s = malloc(10);
free(s);
printf("%s", s); // acessa memória liberada — undefined behavior
Code
// Go — GC previne liberação prematura, mas com custo em runtime
// O GC garante que s não será liberada enquanto ainda houver referências
s := "blueprint"
fmt.Println(s) // sempre seguro, mas o GC adiciona overhead
Code
// Rust — erro em tempo de compilação, zero custo em runtime
let s = String::from("blueprint");
drop(s); // libera explicitamente
println!("{}", s); // ❌ erro de compilação — o compilador recusa
// esse código nunca chega em produção

This is the fundamental difference: C discovers the problem in production (when it has already caused damage). Go prevents it at runtime with a GC (but pays the latency cost). Rust prevents it at compile time — at zero runtime cost.

What the compiler guarantees for you

  • Ownership — each value has an owner. When the owner goes out of scope, the memory is freed automatically.

  • Move semantics — transferring a value invalidates the original variable. Double-free is impossible.

  • Borrowing — referencing without transferring. Immutable: as many as you want. Mutable: only one at a time.

  • No data race — two simultaneous mutable references will not compile. Safe concurrency by design.

  • Lifetimes — the compiler verifies that no reference outlives the value it points to. Dangling pointers are impossible.

  • Zero runtime cost — all of this is verified at compile time. In production, the binary is as fast as C.