Programação & Dev

Gerenciamento de memória no Rust: ownership, borrowing e lifetimes sem mistério

C te dá controle total — e uma arma apontada pro próprio pé. Go resolve com Garbage Collector — e picos de latência imprevisíveis. O Rust encontrou um terceiro caminho: segurança de memória garantida pelo compilador, sem custo em runtime.

Gerenciamento de memória no Rust: ownership, borrowing e lifetimes sem mistério

O problema que cada linguagem resolve diferente

Toda linguagem precisa responder a uma pergunta fundamental: quando a memória que um valor ocupava pode ser liberada? A resposta define tudo — performance, segurança, previsibilidade.

C / C++ — controle manual

Você aloca e libera. Máxima performance, máxima responsabilidade. Esqueceu um free(): memory leak. Liberou duas vezes: double-free. Acessou depois de liberar: use-after-free. 70% das vulnerabilidades da Microsoft vieram daqui.

Go — Garbage Collector

O runtime decide. Você não gerencia memória — o GC varre e libera o que não está mais em uso. Seguro, mas com custo: pausas periódicas imprevisíveis. O Discord migrou porque o GC causava picos a cada 2 minutos.

Rust — Ownership

O compilador decide. Regras verificadas em tempo de compilação garantem que a memória seja liberada exatamente quando deve — sem GC, sem gerenciamento manual, sem surpresas em runtime.

Ownership: cada valor tem um dono

O sistema de ownership do Rust tem três regras simples. Toda a segurança de memória da linguagem deriva delas.

1
Cada valor tem exatamente um dono

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

2
Só pode existir um dono por vez

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

3
Quando o dono sai de escopo, o valor é liberado

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

Na prática, isso fica assim:

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

Em C ou Go, esse código funcionaria — e você teria dois ponteiros apontando pro mesmo dado na memória. Em Rust, o compilador recusa na hora. Não existe chance de double-free ou uso acidental de um valor já transferido.

Borrowing: usar sem ser dono

Se ownership fosse a única forma de acessar dados, você teria que transferir a propriedade toda vez que passasse algo pra uma função — e receber de volta depois. Inviável.

É aí que entra o borrowing: você empresta uma referência ao valor sem transferir o 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

Referências mutáveis — e a regra mais importante do borrowing

Você pode emprestar mutabilidade também — mas com uma restrição crucial:

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

Essa regra elimina data races em tempo de compilação. Em Go ou C, dois threads podendo escrever no mesmo dado ao mesmo tempo é uma bomba-relógio. Em Rust, o compilador simplesmente não deixa isso acontecer.

Lifetimes: quando o compilador precisa de ajuda

O compilador do Rust consegue inferir a maioria das situações de ownership e borrowing automaticamente. Mas existe um caso onde ele precisa de uma dica explícita: quando uma função retorna uma referência, e o compilador precisa saber de qual dos parâmetros ela vem.

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 }}

O 'a é uma anotação de lifetime. Ela não cria nada — só diz ao compilador: "a referência retornada vive pelo mesmo tempo que os dois parâmetros." Com essa informação, ele consegue verificar que você nunca vai retornar uma referência pra algo que já foi liberado.

Comparando na prática: o mesmo bug em três linguagens

Use-after-free é uma das vulnerabilidades mais comuns e perigosas. Veja como cada linguagem lida com ela:

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

Essa é a diferença fundamental: C descobre o problema em produção (quando já causou dano). Go previne em runtime com GC (mas paga o custo de latência). Rust previne em compile time — sem nenhum custo em runtime.