This content is only available in Portuguese.

Not translated yet for this language.

Code & Development

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.

Equipe Blueprintblog7 min
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 s2

println!("{}", s1); // ❌ erro de compilação: s1 não é mais válida
println!("{}", 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.

Por que String e não &str? Tipos que vivem na stack (como inteiros) são copiados automaticamente — são baratos o suficiente. Tipos que vivem na heap (como String) transferem ownership. Se você quer copiar uma String, precisa chamar .clone() explicitamente — o que deixa o custo da operação visível no código.

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ência

println!("{} 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 aqui

let 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.

A regra completa do borrowing: você pode ter qualquer número de referências imutáveis ou exatamente uma referência mutável — nunca os dois ao mesmo tempo. Isso é suficiente pra eliminar toda uma categoria de bugs de concorrência.

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 retornada
fn 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 y
fn 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.

Lifetimes são o ponto de maior atrito em Rust. A boa notícia: o compilador precisou de anotações explícitas em casos bem mais simples antigamente. Versões modernas do Rust inferem lifetimes na maioria dos casos com as lifetime elision rules — você só precisa anotar quando a situação é genuinamente ambígua.

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ências
s := "blueprint"
fmt.Println(s) // sempre seguro, mas o GC adiciona overhead
text
// 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

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.

O que o compilador garante pra você

  • Ownership — cada valor tem um dono. Quando o dono sai de escopo, a memória é liberada automaticamente.
  • Move semantics — transferir um valor invalida a variável original. Double-free é impossível.
  • Borrowing — referenciar sem transferir. Imutável: quantas quiser. Mutável: só uma por vez.
  • Sem data race — duas referências mutáveis simultâneas não compilam. Concorrência segura por design.
  • Lifetimes — o compilador verifica que nenhuma referência sobrevive ao valor que ela aponta. Dangling pointers são impossíveis.
  • Zero custo em runtime — tudo isso é verificado em compilação. Em produção, o binário é tão rápido quanto C.

Article tags

Related articles

Get the latest articles delivered to your inbox.

Follow Us: