tenequm/rust-dev

Guia do primeiro dia para construir bem em Rust - ownership, erros como valores, String vs &str, Box/Rc/Arc, anyhow vs thiserror e uma lista de crates recomendados (tokio, serde, axum, sqlx). Use ao iniciar um projeto Rust, lutar contra o borrow checker ou escolher crates.

O que é rust-dev?

Esta skill fornece uma base prática para escrever aplicações Rust de forma eficaz desde o primeiro commit. Ela não é um livro-texto, mas um guia focado nas diferenças essenciais do Rust em relação a outras linguagens, nas decisões do primeiro dia que moldam todo o projeto e em um conjunto mínimo de crates que cobrem a maioria das aplicações reais. A skill cobre os conceitos fundamentais do modelo mental do Rust (ownership, borrowing, erros como valores, traits) e oferece orientações claras para evitar armadilhas comuns, especialmente para quem vem de linguagens como Python, JavaScript, Go, Java ou C++. Inclui uma tabela de decisão prática para escolhas como `String` vs `&str`, `Box`/`Rc`/`Arc` e `anyhow` vs `thiserror`, além de uma lista de crates essenciais como `tokio`, `serde`, `clap` e `axum`. A skill é compatível com qualquer editor que suporte rust-analyzer e é ideal para iniciantes em Rust ou desenvolvedores experientes que desejam configurar um novo projeto com boas práticas desde o início.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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 (anyhow vs thiserror)
  • Decidindo quais crates usar
  • Configurando um Cargo.toml mí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 para b. a não existe mais.
  • Emprestá-lo imutavelmente: &a permite que outros o vejam. Muitos leitores permitidos.
  • Emprestá-lo mutavelmente: &mut a permite 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 de f para cada T concreto com que você a chamar (monomorfização, como templates C++). Custo zero de tempo de execução.
  • Dispatch dinâmico é opcional via dyn Trait (tipicamente Box<dyn Trait> ou &dyn Trait). Uma busca em vtable por chamada.
  • Sem herança. Traits se compõem. Se você se pegar usando Deref para "estender" um tipo, pare e use composição ou um enum.
  • Regra do órfão: você pode impl SeuTrait para TipoDeOutrem ou impl 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 &str a String, &[T] a Vec<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 &str em 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ãoPadrãoQuando escolher o outro
String vs &str (campo de struct)StringQuase nunca &str até que você tenha uma razão real e entenda lifetimes
String vs &str (parâmetro de função)&strUse 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áticoBox<dyn Trait> quando você precisar de uma coleção heterogênea (Vec<Box<dyn Animal>>)
Erros em código de aplicaçãoanyhow::Result<T> em toda parte-
Erros em código de bibliotecaEnum derivado com thiserrorNunca 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ódulosMódulos inline até um arquivo ficar longo, depois dividirUm 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 (como String, Vec) move ele. a não é mais utilizável. Use &a para emprestar ou a.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 mut para 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 inventar input2.
  • 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 --release as remove e realiza "complemento de dois com wrapping". Uma suíte de testes que passa ainda pode fazer wrapping em produção, então use checked_*/saturating_*/wrapping_* onde a aritmética pode realmente atingir o limite, em vez de confiar no panico de depuração para encontrá-lo. strict_add e similares (estável desde 1.91) panicam em transbordamento em todas as compilações, e overflow-checks = true sob [profile.release] mantém o comportamento de depuração em produção. E as entre tipos inteiros truncam silenciosamente (300_u32 as u8 é 44) - use u8::try_from(x)? onde o valor pode não caber.
  • Uma String não é indexável. s[0] não compila - veja references/ownership-and-types.md para o que usar em vez disso.

De Go:

  • Erros como valores - mesmo instinto, mas use ? em vez de if 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 e Copy é 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 requerem unsafe para 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.

CrateO que fornece
serde + serde_jsonSerialização. #[derive(Serialize, Deserialize)] e pronto
tokioRuntime assíncrono. #[tokio::main], tokio::spawn, I/O assíncrono
anyhowTipo de erro para aplicação. anyhow::Result<T>, bail!, context()
thiserrorEnums de erro para bibliotecas. #[derive(thiserror::Error)]
clapAnálise de argumentos de CLI. #[derive(Parser)]
axumWeb framework. Focado em torre, composição de middlewares
reqwestCliente HTTP. Bloqueante ou assíncrono, com reqwest::Client
sqlxAcesso a banco de dados. Verificação de consultas em tempo de compilação
tracingLogging e telemetria. tracing::info!, spans, tracing-subscriber
chronoData/hora
uuidIdentificadores únicos universais. Uuid::new_v4()
randNúmeros aleatórios
regexExpressõ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

  1. Configure o toolchain (comando curl acima).
  2. Escreva um CLI simples com clap para ter uma noção do ciclo de compilação.
  3. Leia o capítulo oficial de Ownership do Livro Rust - são ~30 minutos e respondem a maioria de suas perguntas do primeiro dia.
  4. Adicione serde e serde_json assim que você tocar em I/O - a serialização é onde o Rust brilha e onde a frustração com tipos acontece.
  5. Execute cargo clippy após cada mudança. Internalize os avisos; eles ensinam o estilo idiomático.
  6. 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.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Habilidades Relacionadas