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.

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.
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.
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.
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.
Uma variável é a dona do valor que ela armazena. Não existe valor sem dono.
Quando você atribui um valor a outra variável, a propriedade se transfere — a variável original deixa de ser válida.
Sem GC, sem free(). O compilador insere a liberação automaticamente quando a variável deixa de existir.
In practice, it looks like this:
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 donaIn 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.
That is where borrowing comes in: you lend a reference to the value without transferring ownership.
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 transferidaMutable references — and the most important rule of borrowing
You can lend mutability too — but with a crucial restriction:
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 tempolet 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 existeThis 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.
// 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 }}// 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:
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.
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.
/* C — compila, executa, comportamento indefinido */char *s = malloc(10);free(s);printf("%s", s); // acessa memória liberada — undefined behavior// 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// 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çãoThis 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.


