Até agora todos os nossos exemplos viveram em um único arquivo main.rs. Isso funciona para programas pequenos, mas projetos reais têm centenas ou milhares de arquivos, dezenas de colaboradores, e código que precisa ser reutilizado em múltiplos contextos. Rust oferece um sistema de módulos robusto para organizar tudo isso de forma clara e controlada.
Neste artigo vamos entender como Rust organiza código em módulos, arquivos e crates — e como controlar o que é público e o que é privado.
O sistema de módulos de Rust
Rust organiza código em uma hierarquia de três níveis:
Crates são a unidade de compilação — um binário ou uma biblioteca. Cada projeto Cargo é um crate.
Módulos são namespaces dentro de um crate — agrupam código relacionado e controlam visibilidade.
Itens são o conteúdo dos módulos — funções, structs, enums, traits, constantes.
Essa hierarquia forma uma árvore, com a raiz sendo o arquivo principal do crate (main.rs para binários, lib.rs para bibliotecas).
Módulos inline com mod
A forma mais simples de criar um módulo é diretamente no arquivo:
mod matematica {
pub fn somar(a: f64, b: f64) -> f64 {
a + b
}
pub fn subtrair(a: f64, b: f64) -> f64 {
a - b
}
// Função privada — só acessível dentro do módulo
fn interno() -> &'static str {
"função interna"
}
pub mod avancado {
pub fn raiz_quadrada(x: f64) -> f64 {
x.sqrt()
}
pub fn potencia(base: f64, exp: u32) -> f64 {
base.powi(exp as i32)
}
}
}
fn main() {
println!("{}", matematica::somar(3.0, 4.0));
println!("{}", matematica::subtrair(10.0, 3.0));
println!("{}", matematica::avancado::raiz_quadrada(16.0));
println!("{}", matematica::avancado::potencia(2.0, 10));
// matematica::interno(); // ERRO: função privada
}
Saída:
7
7
4
1024
Por padrão, tudo em Rust é privado. A palavra-chave pub torna um item público — acessível fora do módulo onde foi definido. Isso é o inverso de muitas linguagens, onde tudo é público por padrão.
Visibilidade granular
Rust oferece controle fino sobre visibilidade:
mod biblioteca {
// Público para todos
pub struct Livro {
pub titulo: String,
pub autor: String,
preco: f64, // privado mesmo sendo em struct pública
}
impl Livro {
// Construtor público — única forma de criar Livro de fora
pub fn novo(titulo: &str, autor: &str, preco: f64) -> Self {
Livro {
titulo: titulo.to_string(),
autor: autor.to_string(),
preco,
}
}
// Público
pub fn preco_com_desconto(&self, desconto: f64) -> f64 {
self.preco * (1.0 - desconto)
}
// Público apenas para o módulo pai
pub(super) fn preco_interno(&self) -> f64 {
self.preco
}
// Público apenas dentro do crate
pub(crate) fn isbn_gerado(&self) -> String {
format!("978-{:010}", self.titulo.len())
}
}
// Privado ao módulo — não acessível fora
fn validar_preco(preco: f64) -> bool {
preco > 0.0
}
}
fn main() {
let livro = biblioteca::Livro::novo(
"The Rust Programming Language",
"Steve Klabnik",
89.90,
);
println!("Título: {}", livro.titulo);
println!("Autor: {}", livro.autor);
// println!("{}", livro.preco); // ERRO: campo privado
println!("Com 10% de desconto: R$ {:.2}",
livro.preco_com_desconto(0.10));
println!("ISBN: {}", livro.isbn_gerado());
}
As variantes de visibilidade disponíveis são:
pub— público para todospub(crate)— público dentro do crate, privado forapub(super)— público para o módulo paipub(in caminho)— público para um módulo específico- sem modificador — privado ao módulo atual
use — trazendo nomes ao escopo
Escrever matematica::avancado::raiz_quadrada toda vez é verboso. O use traz nomes ao escopo atual:
mod geometria {
pub mod formas {
pub struct Circulo {
pub raio: f64,
}
pub struct Retangulo {
pub largura: f64,
pub altura: f64,
}
impl Circulo {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.raio * self.raio
}
}
impl Retangulo {
pub fn area(&self) -> f64 {
self.largura * self.altura
}
}
}
}
// Trazendo tipos específicos ao escopo
use geometria::formas::{Circulo, Retangulo};
// Alias para evitar conflitos de nome
use std::fmt::Display as Exibivel;
fn main() {
let c = Circulo { raio: 5.0 };
let r = Retangulo { largura: 4.0, altura: 6.0 };
println!("Círculo: {:.2}", c.area());
println!("Retângulo: {:.2}", r.area());
}
Por convenção, ao usar use com funções, importe o módulo pai — não a função diretamente. Isso deixa claro na chamada que a função não é local:
// Preferido para funções:
use std::collections;
collections::HashMap::new();
// Preferido para tipos e traits:
use std::collections::HashMap;
HashMap::new();
use com glob
O glob * importa todos os itens públicos de um módulo:
use geometria::formas::*;
Use com moderação — pode poluir o namespace e tornar difícil saber de onde cada nome vem. É aceitável em testes e em módulos prelude (convenção de bibliotecas para importar os itens mais comuns).
Módulos em arquivos separados
Em projetos reais, cada módulo vai em seu próprio arquivo. O Cargo segue uma convenção clara de estrutura de diretórios:
meu_projeto/
├── Cargo.toml
└── src/
├── main.rs
├── matematica.rs
└── geometria/
├── mod.rs (ou geometria.rs no Rust moderno)
├── circulo.rs
└── retangulo.rs
src/matematica.rs:
pub fn somar(a: f64, b: f64) -> f64 {
a + b
}
pub fn media(valores: &[f64]) -> f64 {
let soma: f64 = valores.iter().sum();
soma / valores.len() as f64
}
src/geometria/circulo.rs:
pub struct Circulo {
pub raio: f64,
}
impl Circulo {
pub fn novo(raio: f64) -> Self {
Circulo { raio }
}
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.raio * self.raio
}
pub fn perimetro(&self) -> f64 {
2.0 * std::f64::consts::PI * self.raio
}
}
src/geometria/retangulo.rs:
pub struct Retangulo {
pub largura: f64,
pub altura: f64,
}
impl Retangulo {
pub fn novo(largura: f64, altura: f64) -> Self {
Retangulo { largura, altura }
}
pub fn area(&self) -> f64 {
self.largura * self.altura
}
pub fn e_quadrado(&self) -> bool {
self.largura == self.altura
}
}
src/geometria/mod.rs (ou src/geometria.rs — Rust 2018+):
pub mod circulo;
pub mod retangulo;
// Re-exportando para facilitar uso externo
pub use circulo::Circulo;
pub use retangulo::Retangulo;
src/main.rs:
mod matematica;
mod geometria;
use geometria::{Circulo, Retangulo};
fn main() {
let soma = matematica::somar(3.0, 4.0);
let media = matematica::media(&[1.0, 2.0, 3.0, 4.0, 5.0]);
println!("Soma: {soma}");
println!("Média: {media}");
let c = Circulo::novo(5.0);
let r = Retangulo::novo(4.0, 6.0);
println!("Círculo — área: {:.2}", c.area());
println!("Retângulo — área: {:.2}", r.area());
println!("É quadrado? {}", r.e_quadrado());
}
A declaração mod matematica; em main.rs diz ao compilador: "procure o módulo matematica em src/matematica.rs ou src/matematica/mod.rs." O arquivo é carregado automaticamente.
Bibliotecas vs binários
Um crate pode ser biblioteca, binário, ou ambos simultaneamente:
meu_projeto/
├── Cargo.toml
└── src/
├── main.rs ← binário (fn main)
└── lib.rs ← biblioteca (sem fn main)
lib.rs é a raiz da biblioteca. Código em lib.rs pode ser usado pelo main.rs do mesmo projeto e por outros projetos que dependem do seu crate.
src/lib.rs:
pub mod calculadora;
pub mod geometria;
// Versão da biblioteca acessível por dependentes
pub const VERSAO: &str = "1.0.0";
src/main.rs:
// Usa a própria biblioteca do projeto
use meu_projeto::geometria::Circulo;
use meu_projeto::VERSAO;
fn main() {
println!("Versão: {VERSAO}");
let c = Circulo::novo(3.0);
println!("Área: {:.2}", c.area());
}
Essa separação é fundamental para criar bibliotecas testáveis — os testes de integração vivem em tests/ e usam a crate como qualquer dependente externo usaria.
O arquivo Cargo.toml
Todo projeto Rust tem um Cargo.toml que define metadados e dependências:
[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome <seu@email.com>"]
description = "Descrição do projeto"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
[dev-dependencies]
# Dependências apenas para testes
criterion = "0.5"
[lib]
name = "meu_projeto"
path = "src/lib.rs"
[[bin]]
name = "meu_projeto"
path = "src/main.rs"
O Cargo gerencia automaticamente o download, compilação e atualização de dependências. O ecossistema de pacotes fica em crates.io — o repositório central de crates Rust.
Múltiplos binários
Um projeto pode ter múltiplos binários em src/bin/:
meu_projeto/
├── Cargo.toml
└── src/
├── lib.rs
└── bin/
├── servidor.rs
├── cliente.rs
└── migrador.rs
Cada arquivo em src/bin/ torna-se um binário separado, executável com:
cargo run --bin servidor
cargo run --bin cliente
cargo build --bin migrador
Um projeto completo organizado
Vamos ver como ficaria um projeto de gerenciamento financeiro organizado em módulos:
Estrutura:
financas/
├── Cargo.toml
└── src/
├── main.rs
├── lib.rs
├── conta.rs
├── transacao.rs
└── relatorio.rs
src/transacao.rs:
#[derive(Debug, Clone)]
pub enum TipoTransacao {
Credito,
Debito,
}
#[derive(Debug, Clone)]
pub struct Transacao {
pub descricao: String,
pub valor: f64,
pub tipo: TipoTransacao,
}
impl Transacao {
pub fn credito(descricao: &str, valor: f64) -> Self {
Transacao {
descricao: descricao.to_string(),
valor,
tipo: TipoTransacao::Credito,
}
}
pub fn debito(descricao: &str, valor: f64) -> Self {
Transacao {
descricao: descricao.to_string(),
valor,
tipo: TipoTransacao::Debito,
}
}
pub fn valor_liquido(&self) -> f64 {
match self.tipo {
TipoTransacao::Credito => self.valor,
TipoTransacao::Debito => -self.valor,
}
}
}
src/conta.rs:
use crate::transacao::Transacao;
#[derive(Debug)]
pub struct Conta {
pub titular: String,
pub saldo: f64,
transacoes: Vec<Transacao>,
}
impl Conta {
pub fn nova(titular: &str, saldo_inicial: f64) -> Self {
Conta {
titular: titular.to_string(),
saldo: saldo_inicial,
transacoes: Vec::new(),
}
}
pub fn registrar(&mut self, transacao: Transacao) {
self.saldo += transacao.valor_liquido();
self.transacoes.push(transacao);
}
pub fn historico(&self) -> &[Transacao] {
&self.transacoes
}
pub fn total_creditos(&self) -> f64 {
self.transacoes.iter()
.filter(|t| matches!(t.tipo, crate::transacao::TipoTransacao::Credito))
.map(|t| t.valor)
.sum()
}
pub fn total_debitos(&self) -> f64 {
self.transacoes.iter()
.filter(|t| matches!(t.tipo, crate::transacao::TipoTransacao::Debito))
.map(|t| t.valor)
.sum()
}
}
src/relatorio.rs:
use crate::conta::Conta;
use crate::transacao::TipoTransacao;
pub fn exibir_extrato(conta: &Conta) {
println!("══════════════════════════════════════");
println!(" Extrato — {}", conta.titular);
println!("══════════════════════════════════════");
for t in conta.historico() {
let simbolo = match t.tipo {
TipoTransacao::Credito => "+",
TipoTransacao::Debito => "-",
};
println!(" [{simbolo}] {:<25} R$ {:>8.2}",
t.descricao, t.valor);
}
println!("──────────────────────────────────────");
println!(" Total créditos : R$ {:>8.2}", conta.total_creditos());
println!(" Total débitos : R$ {:>8.2}", conta.total_debitos());
println!(" Saldo atual : R$ {:>8.2}", conta.saldo);
println!("══════════════════════════════════════");
}
src/main.rs:
mod conta;
mod transacao;
mod relatorio;
use conta::Conta;
use transacao::Transacao;
use relatorio::exibir_extrato;
fn main() {
let mut minha_conta = Conta::nova("Ana Silva", 1000.0);
minha_conta.registrar(Transacao::credito("Salário", 5000.0));
minha_conta.registrar(Transacao::debito("Aluguel", 1500.0));
minha_conta.registrar(Transacao::debito("Supermercado", 450.0));
minha_conta.registrar(Transacao::credito("Freelance", 800.0));
minha_conta.registrar(Transacao::debito("Conta de luz", 180.0));
minha_conta.registrar(Transacao::debito("Internet", 99.90));
exibir_extrato(&minha_conta);
}
Saída:
══════════════════════════════════════
Extrato — Ana Silva
══════════════════════════════════════
[+] Salário R$ 5000.00
[-] Aluguel R$ 1500.00
[-] Supermercado R$ 450.00
[+] Freelance R$ 800.00
[-] Conta de luz R$ 180.00
[-] Internet R$ 99.90
──────────────────────────────────────
Total créditos : R$ 5800.00
Total débitos : R$ 2229.90
Saldo atual : R$ 4570.10
══════════════════════════════════════
Caminhos e crate::
Dentro de módulos, você pode referenciar outros itens do mesmo crate usando caminhos absolutos começando com crate:::
// Caminho absoluto — sempre funciona
use crate::transacao::Transacao;
// Caminho relativo — funciona a partir do módulo atual
use super::transacao::Transacao; // volta um nível
use self::utilidades::formatar; // nível atual
A preferência idiomática é usar caminhos absolutos com crate:: — são mais explícitos e não quebram quando você reorganiza módulos.
O que vem a seguir
Com módulos dominados, você tem as ferramentas para organizar qualquer projeto de tamanho real. Nos próximos artigos vamos começar a explorar territórios mais avançados: testes automatizados, concorrência, e as crates mais importantes do ecossistema Rust.
O arco dos primeiros 15 artigos cobre o núcleo da linguagem — ownership, tipos, traits, generics, lifetimes, e organização. Daqui em diante vamos aplicar esses fundamentos em contextos cada vez mais ricos e realistas.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 7 — Managing Growing Projects with Packages, Crates, and Modules — https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
- Rust by Example — Modules — https://doc.rust-lang.org/rust-by-example/mod.html
- The Cargo Book — guia completo do gerenciador de pacotes — https://doc.rust-lang.org/cargo/
- crates.io — repositório central de crates Rust — https://crates.io
- "Rust Module System Explained" — sheshbabu.com — guia visual excelente para iniciantes — https://www.sheshbabu.com/posts/rust-module-system/
- Rustlings, seção
modules— https://github.com/rust-lang/rustlings