Desenvolvimento Rust - Primeiro Dia
Uma base prática para escrever aplicações Rust bem desde o primeiro commit. Não é um livro-texto. Foca nas diferenças em relação a outras linguagens, nas decisões do primeiro dia que moldam tudo e no pequeno conjunto de crates que cobre a maioria das aplicações reais.
Quando Usar
- Iniciando um novo projeto Rust (CLI, serviço, biblioteca)
- Chegando ao Rust vindo de Python, JavaScript, Go, Java/C# ou C++
- Escolhendo entre tipos próprios/emprestados, smart pointers, trait objects vs genéricos
- Selecionando a estratégia de tratamento de erros (
anyhowvsthiserror) - Decidindo quais crates usar
- Configurando um
Cargo.tomlmínimo, mas opinativo, clippy e rustfmt
Configuração do Primeiro Dia
# 1. Instale a toolchain (rustup é o gerenciador da toolchain)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Confirme os componentes (rustfmt e clippy vêm com o stable, rust-src habilita funcionalidades da IDE)
rustup component add rustfmt clippy rust-src
# 3. Crie um projeto
cargo new my-app # binário (src/main.rs)
cargo new --lib my-lib # biblioteca (src/lib.rs)
# 4. O ciclo de desenvolvimento (memorize estes quatro)
cargo check # verificação rápida de tipos, sem geração de código
cargo run # compila e executa (binário)
cargo test # compila e executa testes (incl. doctests)
cargo clippy --all-targets # lint, incl. testes/ e benches/ (execute antes de enviar)
cargo fmt # formata
# 5. Gerencie dependências sem editar o Cargo.toml manualmente
cargo add tokio --features full
cargo remove tokio
cargo update # recalcula o Cargo.lock dentro dos ranges semver existentes
cargo update só se move dentro dos ranges de versão já presentes no Cargo.toml. Atravessar uma versão principal (de 1.x para 2.0) requer uma edição no Cargo.toml ou cargo add <crate>@2.
rust-analyzer é obrigatório. É o servidor de linguagem que todo editor usa (VS Code, Zed, Neovim, Helix; o RustRover usa seu próprio motor, mas é comparável). No VS Code, instale a extensão rust-analyzer e defina rust-analyzer.check.command como "clippy" para receber feedback de lint ao salvar.
Quer um observador de arquivos depois? cargo install --locked bacon, depois execute bacon no seu projeto. Não é necessário no primeiro dia.
O Modelo Mental do Rust em 5 Ideias
Rust troca duas coisas que você considera garantidas na maioria das linguagens (um garbage collector e exceções) por garantias em tempo de compilação sobre memória, condições de corrida e tratamento de erros. A forma da linguagem decorre dessa troca.
1. Ownership: todo valor tem exatamente um dono
Pense em valores como objetos físicos. Um livro, um arquivo, uma conexão de rede. A qualquer momento, uma variável é dona dele. Você pode:
- Movê-lo:
let b = a;transfere a posse parab.anão existe mais. - Emprestá-lo imutavelmente:
&apermite que outros o vejam. Muitos leitores permitidos. - Emprestá-lo mutavelmente:
&mut apermite que uma pessoa o modifique. Acesso exclusivo. - Cloná-lo:
a.clone()faz uma cópia profunda. Ambos mantêm sua própria cópia.
Quando o dono sai de escopo, o valor é descartado (memória liberada, arquivo fechado, bloqueio liberado). Sem GC, sem free manual. Isso é RAII, imposto pelo compilador.
2. Aliasing XOR mutabilidade
A qualquer momento, um dado tem ou:
- uma referência mutável (
&mut T), ou - qualquer número de referências imutáveis (
&T),
nunca ambos. Esta única regra elimina condições de corrida e a maioria dos bugs de uso após liberação. O borrow checker a impõe. Quando ele reclama, está dizendo que sua história de posse dos dados não está clara, não que a linguagem está sendo difícil.
3. Erros são valores, não exceções
Não há try/catch. Funções que podem falhar retornam Result<T, E>. Funções que podem não retornar nada útil retornam Option<T>. O compilador força você a lidar com ambos. O operador ? propaga erros pela pilha de chamadas com um caractere:
fn read_config() -> Result<Config, anyhow::Error> {
let text = std::fs::read_to_string("config.toml")?; // ? = retorno antecipado em Err
let config = toml::from_str(&text)?;
Ok(config)
}
Não há null. Option<T> é None ou Some(value). O compilador não deixará você esquecer o caso None.
4. Traits não são interfaces do Java
Um trait define comportamento. Tipos implmentam traits. Até aqui, familiar. As diferenças:
- Dispatch estático é o padrão. Quando você escreve
fn f<T: Display>(x: T), o compilador gera uma cópia separada defpara cadaTconcreto com que você a chamar (monomorfização, como templates C++). Custo zero de tempo de execução. - Dispatch dinâmico é opcional via
dyn Trait(tipicamenteBox<dyn Trait>ou&dyn Trait). Uma busca em vtable por chamada. - Sem herança. Traits se compõem. Se você se pegar usando
Derefpara "estender" um tipo, pare e use composição ou um enum. - Regra do órfão: você pode
impl SeuTrait para TipoDeOutremouimpl TraitDeOutrem para SeuTipo, mas não ambos estrangeiros. Isso mantém a resolução de dependências sã.
5. O borrow checker é um oráculo de design
O erro mais comum de iniciantes é tratar erros do compilador como obstáculos a silenciar. Não são. Quase todo erro de borrow checker revela um problema real sobre quem é dono do quê. Quando você emperrar, a pergunta raramente é "como faço isso compilar" e quase sempre é "qual é a relação de posse real que quero aqui?" Leia o erro. O compilador é excepcionalmente informativo.
As 3 Perguntas para Cada Assinatura de Função
Antes de escrever uma função, pergunte: ela precisa possuir, ler ou modificar a entrada?
fn consume(s: String) // possui: a função assume a responsabilidade, quem chama a perde
fn read(s: &str) // lê: a função olha para ele, quem chama o mantém
fn modify(s: &mut String) // modifica: a função o altera no lugar
Padrões que funcionam 90% das vezes:
- Parâmetros de função: prefira
&straString,&[T]aVec<T>(são slices, aceitam tanto quem chama com posse quanto emprestado). - Retornos de função: retorne tipos com posse (
String,Vec<T>). Retornar referências significa lifetimes; evite até precisar. - Campos de struct: prefira tipos com posse (
String,Vec<T>). Armazenar&strem uma struct é a armadilha mais comum de iniciantes e cascateia anotações de lifetime por todo tipo que contiver sua struct.
Tabela de Decisão do Primeiro Dia
Respostas de uma linha para as escolhas que surgem primeiro.
| Decisão | Padrão | Quando escolher o outro |
|---|---|---|
String vs &str (campo de struct) | String | Quase nunca &str até que você tenha uma razão real e entenda lifetimes |
String vs &str (parâmetro de função) | &str | Use String apenas se você precisar possuir/armazenar internamente |
Vec<T> vs &[T] (parâmetro) | &[T] | Vec<T> apenas se você precisar possuir |
Box<T> vs Rc<T> vs Arc<T> | Box<T> (dono único, heap) | Arc<T> para posse compartilhada entre threads. Evite Rc<T> como padrão; use Arc<T> para não precisar refatorar quando for usar async |
RefCell<T> vs Mutex<T> | Mutex<T> (ou RwLock<T>) | Mesma razão: funciona em async/threads, enquanto RefCell não |
Option<T> vs Result<T, E> | Option<T> para "sem valor", Result<T, E> para "falhou por uma razão" | Se a ausência carrega significado que quem chama deve tratar, Result |
dyn Trait vs impl Trait / <T: Trait> | Genérico (<T: Trait> ou impl Trait) - dispatch estático | Box<dyn Trait> quando você precisar de uma coleção heterogênea (Vec<Box<dyn Animal>>) |
| Erros em código de aplicação | anyhow::Result<T> em toda parte | - |
| Erros em código de biblioteca | Enum derivado com thiserror | Nunca Box<dyn Error> em APIs públicas de bibliotecas - força quem chama a fazer downcast |
&self vs &mut self vs self | &self para getters, &mut self para setters, self para builders/operações consumidoras | - |
| Layout de módulos | Módulos inline até um arquivo ficar longo, depois dividir | Um módulo = um arquivo é um instinto Java/C#, não Rust |
Expressões Idiomáticas para Internalizar Cedo
Estas aparecem em quase todo programa Rust. Aprenda-as na primeira semana.
? para propagação de erros. Substitui nove linhas de match por um caractere.
let body = reqwest::get(url).await?.text().await?;
Cadeias de iteradores em vez de loops manuais. Compilam para o mesmo código de máquina que loops escritos à mão (LLVM inlineia closures). Rust idiomático é funcional em estilo.
let active_emails: Vec<String> = users
.iter()
.filter(|u| u.active)
.map(|u| u.email.clone())
.collect();
Exaustividade do match. Adicione uma nova variante a um enum e todo match que não a tratar se torna um erro de compilação. Use isto. É uma das ferramentas de refatoração mais poderosas em qualquer linguagem.
Deixe o compilador conduzir refatorações. O mesmo truque funciona com structs: adicione um campo obrigatório sem Default e todo construtor existente se torna um erro de compilação - uma lista de trabalho exaustiva e gratuita de todos os locais a atualizar.
if let e let else para o caso comum de braço único de match.
if let Some(name) = user.name { println!("oi {name}"); }
let Some(name) = user.name else { return Err(anyhow!("sem nome")); };
// `name` está no escopo daqui em diante, sem aninhamento
From / Into para conversões de tipo. Implemente From, obtenha Into gratuitamente. ? usa From para converter tipos de erro automaticamente.
Combinadores em Option / Result. Recorra a .map, .and_then, .unwrap_or, .unwrap_or_else, .ok_or antes de recorrer a match.
Macros de derivação. #[derive(Debug, Clone, PartialEq)] obtém 80% do código repetitivo gratuitamente. Adicione #[derive(Serialize, Deserialize)] para JSON.
Açúcar recente. Funcionalidades estabilizadas que vale a pena conhecer, da mais nova para a mais antiga: bool::ok_or e ok_or_else (1.98) transformam uma condição diretamente em um Result (ready.ok_or(Error::NotReady)?). cfg_select! é um match em tempo de compilação sobre predicados cfg, substituindo a maioria dos usos do crate cfg-if, e guards if let funcionam em braços de match (match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... }) - ambos 1.95. assert_matches! e debug_assert_matches! (1.96) assertam em um padrão em vez de igualdade, que é a asserção natural para um enum. Duas mais antigas que você verá constantemente e não deve confundir com exóticas: let-chains (if let Some(x) = a && x > 3 { ... }), estável na edição 2024 desde 1.88, e closures assíncronas (async || { ... }, com a família de bounds AsyncFn), estável desde 1.85.
Vindo de X, Aqui Está o que Te Morde
De Python ou JavaScript:
let b = a;para um valor no heap (comoString,Vec) move ele.anão é mais utilizável. Use&apara emprestar oua.clone()para copiar.- Sem null.
Option<T>é imposto a você. - Sem exceções.
Result<T, E>e?. O compilador não deixará você ignorar erros. - Sem herança. Composição + traits + enums.
- Variáveis são imutáveis por padrão. Adicione
mutpara mutar. O mesmo para referências:&vs&mut. - Redeclarar um nome é permitido e idiomático:
let input = input.trim();sombreia a ligação antiga com uma nova, possivelmente de um tipo diferente. Não é mutação, e é como o código Rust converte um valor no lugar sem inventarinput2. - Tipos inteiros são explícitos e indexação requer
usize. Eles também transbordam de forma diferente dependendo da compilação: depuração "inclui verificações de transbordamento de inteiros que fazem seu programa panicar em tempo de execução", enquanto--releaseas remove e realiza "complemento de dois com wrapping". Uma suíte de testes que passa ainda pode fazer wrapping em produção, então usechecked_*/saturating_*/wrapping_*onde a aritmética pode realmente atingir o limite, em vez de confiar no panico de depuração para encontrá-lo.strict_adde similares (estável desde 1.91) panicam em transbordamento em todas as compilações, eoverflow-checks = truesob[profile.release]mantém o comportamento de depuração em produção. Easentre tipos inteiros truncam silenciosamente (300_u32 as u8é44) - useu8::try_from(x)?onde o valor pode não caber. - Uma
Stringnão é indexável.s[0]não compila - vejareferences/ownership-and-types.mdpara o que usar em vez disso.
De Go:
- Erros como valores - mesmo instinto, mas use
?em vez deif err != nil. - Sem
nil.Option<T>. - Sem GC e sem goroutines: ownership + borrowing, async/await com
tokio. O modelo async é cooperativo (awaité um ponto de yield explícito), não preemptivo. interface{}se torna traits. Padrão para genéricos com dispatch estático;Box<dyn Trait>apenas quando necessário.- Ligação estática é o padrão. Binários são maiores, mas autocontidos.
panic!deve ser reservado para bugs irrecuperáveis em código de aplicação; não use como "log e continue" ao estilo Go.
De Java ou C#:
- Traits não são interfaces com dispatch virtual por padrão.
<T: Trait>é monomorfizado.dyn Traité a versão dinâmica opcional. - Sem referências nulas.
Option<T>. - Sem exceções.
Result<T, E>e?. - Sem herança de classes. Use enums para tipos soma, traits para comportamento compartilhado.
- Sem GC: ownership e borrowing decidem lifetimes.
Arc<T>é o mais próximo de uma referência Java. - Genéricos são monomorfizados, não apagados por tipo.
De C++:
- Como RAII, mas o borrow checker o impõe em tempo de compilação.
- Sem construtores de cópia/movimento.
Cloneé explícito eCopyé um trait marcador para cópias bit a bit baratas. - Sem comportamento indefinido em código seguro (em teoria).
&é um empréstimo verificado em tempo de compilação, não um ponteiro bruto. Ponteiros brutos existem (*const T,*mut T), mas requeremunsafepara dereferenciar.- Smart pointers são
Box<T>(unique_ptr),Rc<T>(shared_ptr, single thread),Arc<T>(shared_ptr, thread-safe). - Macros são higiênicas. Macros procedurais (derive, atributo, semelhantes a funções) são como
serde,tokio::main, etc. funcionam.
A Lista de Crates Recomendados
Estes cobrem a maioria das aplicações reais. Adicione-os conforme necessário; não são todos obrigatórios.
| Crate | O que fornece |
|---|---|
serde + serde_json | Serialização. #[derive(Serialize, Deserialize)] e pronto |
tokio | Runtime assíncrono. #[tokio::main], tokio::spawn, I/O assíncrono |
anyhow | Tipo de erro para aplicação. anyhow::Result<T>, bail!, context() |
thiserror | Enums de erro para bibliotecas. #[derive(thiserror::Error)] |
clap | Análise de argumentos de CLI. #[derive(Parser)] |
axum | Web framework. Focado em torre, composição de middlewares |
reqwest | Cliente HTTP. Bloqueante ou assíncrono, com reqwest::Client |
sqlx | Acesso a banco de dados. Verificação de consultas em tempo de compilação |
tracing | Logging e telemetria. tracing::info!, spans, tracing-subscriber |
chrono | Data/hora |
uuid | Identificadores únicos universais. Uuid::new_v4() |
rand | Números aleatórios |
regex | Expressões regulares. Compiladas em tempo de compilação com regex! |
Armadilhas Comuns do Primeiro Dia
Colocar & em um String existente para obter um &str. &s funciona; &s[..] também. Mas não escreva s.as_str() a menos que esteja passando um &String e a API queira &str.
Chamar clone() para apaziguar o borrow checker. Quase sempre há uma maneira de reestruturar os lifetimes. Clone é um sintoma de posse não resolvida, não uma solução.
Usar String onde &str serve. Especialmente em parâmetros de função. Aceitar &str aceita tanto &String quanto &str; aceitar String força quem chama a possuir.
Ignorar clippy. Ele não é apenas para limpeza; ele captura bugs reais (como &x versus &mut x desnecessário, loops que podem ser iteradores, etc.). Execute cargo clippy --all-targets antes de cada push.
Enrolar tudo em structs e depois lutar com lifetimes. Comece com tipos com posse. Adicione referências apenas quando necessário.
Usar Box<dyn Error> em uma biblioteca. Isso força toda pessoa que chama a fazer downcast. Use thiserror para definir um tipo de erro real.
Esquecer que a inferência de tipos para em cabeçalhos de função. A assinatura precisa declarar tipos. Assista para silêncios quando uma função não compila porque um tipo não pode ser inferido.
Configuração do Cargo.toml do Primeiro Dia
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
rust-version = "1.88" # edição 2024 está estável desde esta versão
[dependencies]
# adicione crates conforme necessário
[profile.release]
# Remove artefatos de depuração para reduzir tamanho do binário
strip = true
# Mantém verificações de transbordamento mesmo em release (opcional)
overflow-checks = true
Clippy e Rustfmt
# .clippy.toml (opcional)
# flag-level configurações vão aqui
# rustfmt.toml (opcional - valores padrão são razoáveis)
imports_granularity = "Crate"
edition = "2024"
Comece Aqui
- Configure o toolchain (comando curl acima).
- Escreva um CLI simples com
clappara ter uma noção do ciclo de compilação. - Leia o capítulo oficial de Ownership do Livro Rust - são ~30 minutos e respondem a maioria de suas perguntas do primeiro dia.
- Adicione
serdeeserde_jsonassim que você tocar em I/O - a serialização é onde o Rust brilha e onde a frustração com tipos acontece. - Execute
cargo clippyapós cada mudança. Internalize os avisos; eles ensinam o estilo idiomático. - Escreva um teste para a primeira função não trivial. Testes são cidadãos de primeira classe - nenhum framework externo é necessário.
O Rust é diferente, não mais difícil. A diferença é paga adiantada: você investe em projeto explícito de ownership e tipos no primeiro dia, e recebe de volta garantias de correção que nenhuma outra linguagem de produção oferece. Boa codificação.