Padrões de Desenvolvimento Rust
Padrões idiomáticos Rust e melhores práticas para construir aplicações seguras, performáticas e de fácil manutenção.
Quando Usar
- Escrevendo novo código Rust
- Revisando código Rust
- Refatorando código Rust existente
- Projetando estrutura de crate e layout de módulos
Como Funciona
Esta habilidade reforça convenções idiomáticas Rust em seis áreas principais: ownership e borrowing para prevenir corridas de dados em tempo de compilação, propagação de erros com Result/? usando thiserror para bibliotecas e anyhow para aplicações, enums e pattern matching exaustivo para tornar estados inválidos irrepresentáveis, traits e genéricos para abstração de custo zero, concorrência segura via Arc<Mutex<T>>, canais e async/await, e superfícies pub mínimas organizadas por domínio.
Princípios Centrais
1. Ownership e Borrowing
O sistema de ownership do Rust previne corridas de dados e bugs de memória em tempo de compilação.
// Bom: Passe referências quando não precisar de ownership
fn process(data: &[u8]) -> usize {
data.len()
}
// Bom: Assuma ownership apenas quando precisar armazenar ou consumir
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// Ruim: Clonar desnecessariamente para evitar o borrow checker
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // Desperdício — apenas tome emprestado
cloned.len()
}
Use Cow para Ownership Flexível
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // Custo zero quando nenhuma mutação é necessária
}
}
Tratamento de Erros
Use Result e ? — Nunca unwrap() em Produção
// Bom: Propague erros com contexto
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("falha ao ler config de {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("falha ao analisar config de {path}"))?;
Ok(config)
}
// Ruim: Panico em caso de erro
fn load_config_bad(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // Pânico!
toml::from_str(&content).unwrap()
}
Erros de Biblioteca com thiserror, Erros de Aplicação com anyhow
// Código de biblioteca: erros estruturados e tipados
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("registro não encontrado: {id}")]
NotFound { id: String },
#[error("conexão falhou")]
Connection(#[from] std::io::Error),
#[error("dados inválidos: {0}")]
InvalidData(String),
}
// Código de aplicação: tratamento de erros flexível
use anyhow::{bail, Result};
fn run() -> Result<()> {
let config = load_config("app.toml")?;
if config.workers == 0 {
bail!("contagem de workers deve ser > 0");
}
Ok(())
}
Combinadores de Option ao Invés de Match Aninhado
// Bom: Cadeia de combinadores
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// Ruim: Match profundamente aninhado
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
match users.iter().find(|u| u.id == id) {
Some(user) => match &user.email {
email => Some(email.clone()),
},
None => None,
}
}
Enums e Pattern Matching
Modele Estados como Enums
// Bom: Estados impossíveis são irrepresentáveis
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
Matching Exaustivo — Sem Catch-All para Lógica de Negócio
// Bom: Trate cada variante explicitamente
match command {
Command::Start => start_service(),
Command::Stop => stop_service(),
Command::Restart => restart_service(),
// Adicionar uma nova variante força o tratamento aqui
}
// Ruim: Curinga esconde novas variantes
match command {
Command::Start => start_service(),
_ => {} // Ignora silenciosamente Stop, Restart e variantes futuras
}
Traits e Genéricos
Aceite Genéricos, Retorne Tipos Concretos
// Bom: Entrada genérica, saída concreta
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}
// Bom: Trait bounds para múltiplas restrições
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("processado: {item}")
}
Objetos Trait para Dispatch Dinâmico
// Use quando precisar de coleções heterogêneas ou sistemas de plugins
trait Handler: Send + Sync {
fn handle(&self, request: &Request) -> Response;
}
struct Router {
handlers: Vec<Box<dyn Handler>>,
}
// Use genéricos quando precisar de performance (monomorfização)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
Padrão Newtype para Segurança de Tipos
// Bom: Tipos distintos previnem troca de argumentos
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// Não é possível trocar acidentalmente os IDs de usuário e pedido
todo!()
}
// Ruim: Fácil trocar argumentos
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
Structs e Modelagem de Dados
Padrão Builder para Construção Complexa
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
}
}
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
impl ServerConfigBuilder {
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
fn build(self) -> ServerConfig {
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
}
}
// Uso: ServerConfig::builder("localhost", 8080).max_connections(200).build()
Iteradores e Closures
Prefira Cadeias de Iteradores a Loops Manuais
// Bom: Declarativo, lazy, componível
let active_emails: Vec<String> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.clone())
.collect();
// Ruim: Acumulação imperativa
let mut active_emails = Vec::new();
for user in &users {
if user.is_active {
active_emails.push(user.email.clone());
}
}
Use collect() com Anotação de Tipo
// Colete em diferentes tipos
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();
// Colete Results — curto-circuito no primeiro erro
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
Concorrência
Arc<Mutex<T>> para Estado Mutável Compartilhado
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut num = counter.lock().expect("mutex envenenado");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("thread worker entrou em pânico");
}
Canais para Passagem de Mensagens
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // Canal limitado com backpressure
for i in 0..5 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(format!("mensagem {i}")).expect("receiver desconectado");
});
}
drop(tx); // Feche o sender para que o iterador rx termine
for msg in rx {
println!("{msg}");
}
Async com Tokio
use tokio::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = tokio::time::timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context("requisição excedeu tempo limite")?
.context("requisição falhou")?;
response.text().await.context("falha ao ler corpo")
}
// Dispare tarefas concorrentes
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
let handles: Vec<_> = urls.into_iter()
.map(|url| tokio::spawn(async move {
fetch_with_timeout(&url).await
}))
.collect();
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap_or_else(|e| panic!("tarefa disparada entrou em pânico: {e}")));
}
results
}
Código Inseguro (Unsafe)
Quando Unsafe é Aceitável
// Aceitável: Limite FFI com invariantes documentados (Rust 2024+)
/// # Segurança
/// `ptr` deve ser um ponteiro válido e alinhado para um `Widget` inicializado.
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
// SEGURANÇA: o chamador garante que ptr é válido e alinhado
unsafe { &*ptr }
}
// Aceitável: Caminho crítico de performance com prova de correção
// SEGURANÇA: index é sempre < len devido ao limite do loop
unsafe { slice.get_unchecked(index) }
Quando Unsafe NÃO é Aceitável
// Ruim: Usar unsafe para burlar o borrow checker
// Ruim: Usar unsafe por conveniência
// Ruim: Usar unsafe sem um comentário de Segurança
// Ruim: Transmutar entre tipos não relacionados
Sistema de Módulos e Estrutura de Crate
Organize por Domínio, Não por Tipo
my_app/
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── auth/ # Módulo de domínio
│ │ ├── mod.rs
│ │ ├── token.rs
│ │ └── middleware.rs
│ ├── orders/ # Módulo de domínio
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ └── service.rs
│ └── db/ # Infraestrutura
│ ├── mod.rs
│ └── pool.rs
├── tests/ # Testes de integração
├── benches/ # Benchmarks
└── Cargo.toml
Visibilidade — Exponha Minimamente
// Bom: pub(crate) para compartilhamento interno
pub(crate) fn validate_input(input: &str) -> bool {
!input.is_empty()
}
// Bom: Re-exporte a API pública de lib.rs
pub mod auth;
pub use auth::AuthMiddleware;
// Ruim: Tornando tudo pub
pub fn internal_helper() {} // Deveria ser pub(crate) ou privado
Integração com Ferramentas
Comandos Essenciais
# Compilar e verificar
cargo build
cargo check # Verificação de tipo rápida sem geração de código
cargo clippy # Lints e sugestões
cargo fmt # Formatador de código
# Testes
cargo test
cargo test -- --nocapture # Mostrar saída println
cargo test --lib # Apenas testes unitários
cargo test --test integration # Apenas testes de integração
# Dependências
cargo audit # Auditoria de segurança
cargo tree # Árvore de dependências
cargo update # Atualizar dependências
# Performance
cargo bench # Executar benchmarks
Referência Rápida: Expressões Idiomáticas Rust
| Expressão | Descrição |
|---|---|
| Empreste, não clone | Passe &T em vez de clonar, a menos que ownership seja necessário |
| Torne estados ilegais irrepresentáveis | Use enums para modelar apenas estados válidos |
? em vez de unwrap() | Propague erros, nunca entre em pânico em bibliotecas/código de produção |
| Analise, não valide | Converta dados não estruturados em structs tipados na fronteira |
| Newtype para segurança de tipos | Empacote primitivos em newtypes para evitar troca de argumentos |
| Prefira iteradores a loops | Cadeias declarativas são mais claras e frequentemente mais rápidas |
#[must_use] em Results | Garanta que chamadores tratem valores de retorno |
Cow para ownership flexível | Evite alocações quando o empréstimo for suficiente |
| Matching exaustivo | Sem curinga _ para enums críticos ao negócio |
Superfície pub mínima | Use pub(crate) para APIs internas |
Antipadrões a Evitar
// Ruim: .unwrap() em código de produção
let value = map.get("key").unwrap();
// Ruim: .clone() para satisfazer o borrow checker sem entender o porquê
let data = expensive_data.clone();
process(&original, &data);
// Ruim: Usar String quando &str é suficiente
fn greet(name: String) { /* deveria ser &str */ }
// Ruim: Box<dyn Error> em bibliotecas (use thiserror em vez disso)
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
// Ruim: Ignorar avisos de must_use
let _ = validate(input); // Descartando silenciosamente um Result
// Ruim: Bloqueio em contexto async
async fn bad_async() {
std::thread::sleep(Duration::from_secs(1)); // Bloqueia o executor!
// Use: tokio::time::sleep(Duration::from_secs(1)).await;
}
Lembre-se: Se compila, provavelmente está correto — mas apenas se você evitar unwrap(), minimizar unsafe e deixar o sistema de tipos trabalhar a seu favor.