Code & Development

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.

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

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.

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:

JAVASCRIPT
let s1 = String::from("blueprint");let s2 = s1; // ownership se transfere pra s2println!("{}", s1); // ❌ erro de compilação: s1 não é mais válidaprintln!("{}", 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.

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.

TEXT
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ênciaprintln!("{} 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:

TEXT
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
TEXT
let mut s = String::from("blueprint");{    let r1 = &mut s;    r1.push_str(" blog");} // r1 sai de escopo aquilet 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.

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.

TEXT
// Sem lifetime — o compilador não sabe de onde vem a referência retornadafn maior(x: &str, y: &str) -> &str { // ❌ erro de compilação    if x.len() > y.len() { x } else { y }}
TEXT
// Com lifetime — o compilador sabe que o retorno vive tanto quanto x e yfn 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.

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.

TEXT
/* C — compila, executa, comportamento indefinido */char *s = malloc(10);free(s);printf("%s", s); // acessa memória liberada — undefined behavior
TEXT
// Go — GC previne liberação prematura, mas com custo em runtime// O GC garante que s não será liberada enquanto ainda houver referênciass := "blueprint"fmt.Println(s) // sempre seguro, mas o GC adiciona overhead
TEXT
// Rust — erro em tempo de compilação, zero custo em runtimelet s = String::from("blueprint");drop(s); // libera explicitamenteprintln!("{}", 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.