Sistema de Módulos Rust: mod, use, Crates | Rust Brasil

Guia do sistema de módulos em Rust: mod, use, pub, re-exports, paths, mod.rs e organização de workspaces com Cargo.

O sistema de módulos do Rust é poderoso, mas pode ser confuso para iniciantes — especialmente quem vem de linguagens onde a estrutura de arquivos determina automaticamente os módulos. Em Rust, a hierarquia de módulos é explícita: você precisa declarar cada módulo e suas relações. Neste artigo, vamos desmistificar mod, use, pub e a organização de projetos, desde um arquivo único até workspaces multi-crate.

Conceitos Fundamentais

Crate, Módulo e Path

Hierarquia do sistema de módulos:
──────────────────────────────────
Crate (unidade de compilação)
  └── Módulo raiz (lib.rs ou main.rs)
      ├── Módulo A
      │   ├── Submódulo A1
      │   └── Submódulo A2
      └── Módulo B
          └── Submódulo B1

Paths (caminhos para itens):
  crate::modulo_a::submodulo_a1::funcao()
  self::funcao_local()
  super::funcao_do_pai()
  • Crate: a unidade de compilação — um binário ou uma biblioteca
  • Módulo: um namespace que agrupa itens relacionados
  • Path: o caminho para acessar um item (crate::modulo::item)

Módulos Inline

A forma mais simples de criar módulos: declarar diretamente no arquivo:

mod matematica {
    pub fn somar(a: i32, b: i32) -> i32 {
        a + b
    }

    pub fn multiplicar(a: i32, b: i32) -> i32 {
        a * b
    }

    // Função privada — não acessível fora do módulo
    fn auxiliar() -> i32 {
        42
    }

    // Submódulo
    pub mod avancado {
        pub fn potencia(base: f64, exp: u32) -> f64 {
            (0..exp).fold(1.0, |acc, _| acc * base)
        }
    }
}

fn main() {
    println!("{}", matematica::somar(2, 3));
    println!("{}", matematica::multiplicar(4, 5));
    println!("{}", matematica::avancado::potencia(2.0, 10));

    // matematica::auxiliar(); // ERRO: função privada
}

Módulos em Arquivos Separados

Para projetos maiores, cada módulo vai em seu próprio arquivo. Existem duas convenções:

Convenção Moderna (Rust 2018+): Arquivo por Módulo

meu_projeto/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── config.rs         ← módulo config
    ├── database.rs       ← módulo database
    └── handlers/
        ├── mod.rs        ← módulo handlers (raiz)
        ├── usuarios.rs   ← submódulo handlers::usuarios
        └── produtos.rs   ← submódulo handlers::produtos

Ou a alternativa sem mod.rs (recomendada para novos projetos):

meu_projeto/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── config.rs
    ├── database.rs
    ├── handlers.rs       ← declara submódulos
    └── handlers/
        ├── usuarios.rs
        └── produtos.rs

Implementação

src/main.rs:

mod config;
mod database;
mod handlers;

fn main() {
    let cfg = config::carregar();
    println!("Porta: {}", cfg.porta);

    database::inicializar(&cfg);

    handlers::usuarios::listar();
    handlers::produtos::listar();
}

src/config.rs:

pub struct Config {
    pub porta: u16,
    pub host: String,
    pub database_url: String,
}

pub fn carregar() -> Config {
    Config {
        porta: 8080,
        host: "localhost".to_string(),
        database_url: "postgres://localhost/app".to_string(),
    }
}

src/database.rs:

use crate::config::Config;

pub fn inicializar(config: &Config) {
    println!("Conectando ao banco: {}", config.database_url);
}

pub fn executar_query(sql: &str) -> Vec<String> {
    println!("Executando: {}", sql);
    vec!["resultado1".to_string(), "resultado2".to_string()]
}

src/handlers.rs (declara submódulos):

pub mod usuarios;
pub mod produtos;

src/handlers/usuarios.rs:

use crate::database;

pub fn listar() {
    let resultados = database::executar_query("SELECT * FROM usuarios");
    println!("Usuários: {:?}", resultados);
}

pub fn buscar_por_id(id: u32) -> Option<String> {
    let resultados = database::executar_query(&format!("SELECT * FROM usuarios WHERE id = {}", id));
    resultados.into_iter().next()
}

src/handlers/produtos.rs:

use crate::database;

pub fn listar() {
    let resultados = database::executar_query("SELECT * FROM produtos");
    println!("Produtos: {:?}", resultados);
}

Visibilidade: pub e Suas Variantes

O Rust tem um sistema de visibilidade granular:

mod externo {
    pub mod interno {
        // pub — visível para todos
        pub fn publica() {}

        // (sem pub) — visível apenas dentro deste módulo
        fn privada() {}

        // pub(crate) — visível em qualquer lugar DENTRO do crate
        pub(crate) fn interna_crate() {}

        // pub(super) — visível no módulo pai
        pub(super) fn interna_pai() {}

        // pub(in crate::externo) — visível no caminho especificado
        pub(in crate::externo) fn interna_externo() {}
    }

    pub fn usar_interno() {
        interno::publica();           // OK
        // interno::privada();        // ERRO: privada
        interno::interna_crate();     // OK: mesmo crate
        interno::interna_pai();       // OK: somos o super
        interno::interna_externo();   // OK: estamos em crate::externo
    }
}

fn main() {
    externo::interno::publica();       // OK
    // externo::interno::privada();    // ERRO
    externo::interno::interna_crate(); // OK: mesmo crate
    // externo::interno::interna_pai(); // ERRO: não somos o super
}

Structs com Campos Privados

mod banco {
    pub struct ContaBancaria {
        titular: String,         // privado!
        pub numero: u64,
        saldo: f64,              // privado!
    }

    impl ContaBancaria {
        // Construtor público necessário porque campos são privados
        pub fn nova(titular: &str, numero: u64) -> Self {
            ContaBancaria {
                titular: titular.to_string(),
                numero,
                saldo: 0.0,
            }
        }

        pub fn titular(&self) -> &str {
            &self.titular
        }

        pub fn saldo(&self) -> f64 {
            self.saldo
        }

        pub fn depositar(&mut self, valor: f64) {
            if valor > 0.0 {
                self.saldo += valor;
            }
        }
    }
}

fn main() {
    let mut conta = banco::ContaBancaria::nova("Ana", 12345);
    conta.depositar(1000.0);

    println!("Conta {}: titular={}, saldo=R${:.2}",
        conta.numero,       // OK: campo público
        conta.titular(),    // OK: método público
        conta.saldo(),      // OK: método público
    );
    // conta.saldo = -100.0; // ERRO: campo privado
}

use — Importando Itens

Básico

// Caminho completo — verboso
fn exemplo1() {
    let mapa = std::collections::HashMap::<String, i32>::new();
    println!("{:?}", mapa);
}

// Com use — limpo
use std::collections::HashMap;

fn exemplo2() {
    let mapa = HashMap::<String, i32>::new();
    println!("{:?}", mapa);
}

fn main() {
    exemplo1();
    exemplo2();
}

Importações Múltiplas e Aninhadas

// Importar múltiplos itens do mesmo módulo
use std::collections::{HashMap, HashSet, BTreeMap};

// Importar com renomeação (alias)
use std::collections::HashMap as Mapa;
use std::io::Result as IoResult;

// Importar tudo de um módulo (use com cautela)
// use std::collections::*;

// Importações aninhadas
use std::io::{self, Read, Write, BufRead};
// Equivalente a:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;

fn main() {
    let _: HashMap<String, i32> = HashMap::new();
    let _: HashSet<i32> = HashSet::new();
    let _: BTreeMap<String, i32> = BTreeMap::new();
    let _: Mapa<String, i32> = Mapa::new();
}

Convenções de Importação

// CONVENÇÃO: importar o módulo pai para funções
use std::fmt;
// Uso: fmt::Display, fmt::Formatter

// CONVENÇÃO: importar o tipo diretamente para structs/enums/traits
use std::collections::HashMap;
// Uso: HashMap::new()

// CONVENÇÃO: re-exportar no nível adequado
mod modelos {
    pub mod usuario {
        pub struct Usuario {
            pub nome: String,
        }
    }
}

// Re-exportar para simplificar o acesso
pub use modelos::usuario::Usuario;

fn main() {
    let u = Usuario { nome: "Ana".into() };
    println!("{}", u.nome);
}

Resolução de Paths

crate, self, super

mod nivel1 {
    pub fn funcao_nivel1() -> &'static str {
        "nível 1"
    }

    pub mod nivel2 {
        pub fn funcao_nivel2() -> &'static str {
            "nível 2"
        }

        // super:: refere-se ao módulo pai (nivel1)
        pub fn chamar_pai() -> &'static str {
            super::funcao_nivel1()
        }

        pub mod nivel3 {
            // crate:: começa da raiz do crate
            pub fn chamar_raiz() {
                println!("{}", crate::nivel1::funcao_nivel1());
            }

            // super:: vai um nível acima (nivel2)
            pub fn chamar_nivel2() {
                println!("{}", super::funcao_nivel2());
            }

            // super::super:: vai dois níveis acima (nivel1)
            pub fn chamar_nivel1() {
                println!("{}", super::super::funcao_nivel1());
            }

            // self:: refere-se ao módulo atual
            pub fn local() -> &'static str {
                "nível 3"
            }

            pub fn chamar_local() {
                println!("{}", self::local());
            }
        }
    }
}

fn main() {
    nivel1::nivel2::nivel3::chamar_raiz();
    nivel1::nivel2::nivel3::chamar_nivel2();
    nivel1::nivel2::nivel3::chamar_nivel1();
    nivel1::nivel2::nivel3::chamar_local();
}

Re-exports: Organizando a API Pública

Re-exports (pub use) permitem criar uma API pública limpa independente da estrutura interna:

// Estrutura interna complexa
mod internos {
    pub mod v2 {
        pub mod processamento {
            pub struct Processador {
                pub nome: String,
            }

            impl Processador {
                pub fn novo(nome: &str) -> Self {
                    Processador { nome: nome.to_string() }
                }

                pub fn executar(&self) {
                    println!("Processador '{}' executando", self.nome);
                }
            }
        }

        pub mod erros {
            #[derive(Debug)]
            pub enum AppError {
                NaoEncontrado,
                Permissao,
            }
        }
    }
}

// Re-exports criam uma API pública limpa
pub use internos::v2::processamento::Processador;
pub use internos::v2::erros::AppError;

fn main() {
    // Usuários usam o caminho curto
    let p = Processador::novo("principal");
    p.executar();

    let _erro = AppError::NaoEncontrado;

    // Em vez de:
    // let p = internos::v2::processamento::Processador::novo("principal");
}

Padrão Prelude

Muitas bibliotecas usam um módulo prelude para re-exportar os itens mais usados:

mod tipos {
    pub struct Config {
        pub valor: String,
    }

    pub struct Conexao {
        pub url: String,
    }

    pub trait Executavel {
        fn executar(&self);
    }
}

mod erros {
    #[derive(Debug)]
    pub enum Erro {
        Conexao,
        Timeout,
    }

    pub type Resultado<T> = Result<T, Erro>;
}

// Prelude: re-exporta tudo que o usuário geralmente precisa
pub mod prelude {
    pub use crate::tipos::{Config, Conexao, Executavel};
    pub use crate::erros::{Erro, Resultado};
}

// Uso:
use crate::prelude::*;

impl Executavel for Config {
    fn executar(&self) {
        println!("Executando config: {}", self.valor);
    }
}

fn main() {
    let config = Config { valor: "teste".into() };
    config.executar();

    let resultado: Resultado<i32> = Ok(42);
    println!("{:?}", resultado);
}

Workspaces: Projetos Multi-Crate

Para projetos grandes, o Cargo suporta workspaces — múltiplos crates que compartilham um Cargo.lock:

Estrutura

meu_workspace/
├── Cargo.toml              ← workspace root
├── crates/
│   ├── core/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── lib.rs
│   ├── api/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── main.rs
│   └── cli/
│       ├── Cargo.toml
│       └── src/
│           └── main.rs
└── shared/
    ├── Cargo.toml
    └── src/
        └── lib.rs

Configuração

Cargo.toml (raiz do workspace):

[workspace]
members = [
    "crates/core",
    "crates/api",
    "crates/cli",
    "shared",
]

# Dependências compartilhadas por todos os membros
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

crates/core/Cargo.toml:

[package]
name = "meu-core"
version = "0.1.0"
edition = "2021"

[dependencies]
serde.workspace = true  # usa a versão do workspace

crates/api/Cargo.toml:

[package]
name = "meu-api"
version = "0.1.0"
edition = "2021"

[dependencies]
meu-core = { path = "../core" }  # dependência local
tokio.workspace = true
serde.workspace = true

Comandos Úteis

# Compilar todo o workspace
cargo build

# Compilar apenas um crate
cargo build -p meu-api

# Rodar testes de todo o workspace
cargo test

# Rodar testes de um crate específico
cargo test -p meu-core

# Rodar o binário de um crate
cargo run -p meu-api
cargo run -p meu-cli

Padrão de Organização Recomendado

Para projetos de médio porte, esta é uma organização robusta:

meu_projeto/
├── Cargo.toml
├── src/
│   ├── main.rs           ← ponto de entrada, setup e coordenação
│   ├── lib.rs            ← API pública da biblioteca (opcional)
│   ├── config.rs         ← carregamento de configuração
│   ├── error.rs          ← tipos de erro customizados
│   ├── models/
│   │   ├── mod.rs
│   │   ├── usuario.rs
│   │   └── produto.rs
│   ├── services/
│   │   ├── mod.rs
│   │   ├── auth.rs
│   │   └── pedidos.rs
│   ├── handlers/         ← (para web: endpoints HTTP)
│   │   ├── mod.rs
│   │   ├── api.rs
│   │   └── health.rs
│   └── db/
│       ├── mod.rs
│       ├── pool.rs
│       └── queries.rs
├── tests/                ← testes de integração
│   └── api_test.rs
└── benches/              ← benchmarks
    └── performance.rs

src/main.rs:

mod config;
mod error;
mod models;
mod services;
mod handlers;
mod db;

// Re-exports para uso fácil
use error::AppError;

fn main() -> Result<(), AppError> {
    let cfg = config::carregar()?;
    println!("Servidor iniciando na porta {}", cfg.porta);
    Ok(())
}

src/error.rs:

use std::fmt;

#[derive(Debug)]
pub enum AppError {
    Config(String),
    Database(String),
    NotFound(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Config(msg) => write!(f, "Erro de configuração: {}", msg),
            AppError::Database(msg) => write!(f, "Erro de banco: {}", msg),
            AppError::NotFound(msg) => write!(f, "Não encontrado: {}", msg),
        }
    }
}

impl std::error::Error for AppError {}

src/models/mod.rs:

pub mod usuario;
pub mod produto;

// Re-exports dos tipos principais
pub use usuario::Usuario;
pub use produto::Produto;

Erros Comuns

1. Unresolved Import

// ERRO: mod 'utils' não foi declarado
// use crate::utils::helper;

// SOLUÇÃO: declare o módulo em main.rs ou lib.rs
// mod utils; // ← adicionar isso

Veja Cargo: Unresolved Import para mais detalhes.

2. Esquecer pub em itens exportados

mod meu_modulo {
    // ERRO: struct é pública mas campo é privado
    // pub struct Config {
    //     porta: u16, // privado!
    // }

    // CORRETO
    pub struct Config {
        pub porta: u16,
    }
}

fn main() {
    let c = meu_modulo::Config { porta: 8080 };
    println!("{}", c.porta);
}

3. Confusão entre mod.rs e arquivo com nome do módulo

// Ambos representam o módulo 'handlers':

// Opção 1: handlers/mod.rs
src/
├── handlers/
│   └── mod.rs

// Opção 2: handlers.rs (+ pasta handlers/ para submódulos)
src/
├── handlers.rs
├── handlers/
│   └── sub.rs

// NÃO PODE ter ambos — é um erro de compilação!

4. Caminho circular

// ERRO: módulos não podem ter dependências circulares
// mod a { use crate::b::funcao_b; }
// mod b { use crate::a::funcao_a; }

// SOLUÇÃO: extraia a parte compartilhada para um módulo separado
mod compartilhado {
    pub fn funcao_compartilhada() {}
}

mod a {
    use crate::compartilhado;

    pub fn funcao_a() {
        compartilhado::funcao_compartilhada();
    }
}

mod b {
    use crate::compartilhado;

    pub fn funcao_b() {
        compartilhado::funcao_compartilhada();
    }
}

fn main() {
    a::funcao_a();
    b::funcao_b();
}

Veja Também