Aprende Rust → Lección 9
Todo lo anterior cabía en un archivo. Un programa real, no. Esta lección es la menos conceptual de la guía y la más práctica: cómo se llama cada cosa en un proyecto de Rust, cómo se reparte el código en varios archivos, quién puede ver qué, y cómo se añade una biblioteca de otra persona sin más ceremonia que una línea.
El vocabulario, que es lo que más confunde
Rust usa tres palabras que se solapan y conviene separar antes de nada:
Cargo.toml; un crate es una unidad de compilación; un módulo es una carpeta dentro de un crate.Un proyecto nuevo se crea con una orden, y Cargo deja la estructura mínima hecha:
$ cargo new mi_tienda
Created binary (application) `mi_tienda` package
mi_tienda/
├── Cargo.toml
└── src/
└── main.rs
Con --lib en lugar de nada, Cargo crea un crate de biblioteca con src/lib.rs en vez de un binario. Y las órdenes del día a día son pocas:
| Orden | Qué hace |
|---|---|
cargo build |
Compila en modo de depuración, con comprobaciones |
cargo run |
Compila y ejecuta |
cargo build --release |
Compila optimizado, para entregar |
cargo check |
Comprueba que compila sin generar el binario: mucho más rápido |
cargo test |
Ejecuta las pruebas |
cargo add nombre |
Añade una dependencia y la escribe en Cargo.toml |
cargo fmt |
Formatea el código con el estilo estándar |
cargo clippy |
Avisa de cosas que compilan pero se escriben mejor de otra forma |
cargo check merece una mención aparte: mientras escribes, es el que se usa, porque hace todo el trabajo de comprobación de tipos y préstamos sin pagar el coste de generar código máquina.
Repartir el código en archivos
Un módulo se declara con mod. Puede estar escrito ahí mismo, entre llaves, o vivir en su propio archivo. Entre llaves se ve bien la idea:
mod cocina {
pub fn servir() -> String {
preparar()
}
fn preparar() -> String {
String::from("plato listo")
}
}
fn main() {
println!("{}", cocina::servir());
}
En cuanto crece, el módulo se saca a un archivo. Aquí es donde Rust tiene una regla que hay que aprenderse una vez: declarar mod algo; con punto y coma le dice al compilador que busque el contenido en algo.rs. No hay importaciones por ruta de archivo; el árbol de módulos es el que manda.
mi_tienda/
├── Cargo.toml
└── src/
├── main.rs <- aquí va: mod inventario;
├── inventario.rs <- y aquí: pub mod articulo;
└── inventario/
└── articulo.rs
La carpeta inventario/ guarda los submódulos de inventario. Los tres archivos quedan así:
// src/inventario/articulo.rs
#[derive(Debug)]
pub struct Articulo {
pub nombre: String,
pub precio: f64,
unidades: u32,
}
impl Articulo {
pub fn nuevo(nombre: &str, precio: f64, unidades: u32) -> Articulo {
Articulo {
nombre: String::from(nombre),
precio,
unidades,
}
}
pub fn valor(&self) -> f64 {
self.precio * self.unidades as f64
}
}
// src/inventario.rs
pub mod articulo;
use articulo::Articulo;
pub fn valor_total(articulos: &[Articulo]) -> f64 {
articulos.iter().map(|a| a.valor()).sum()
}
// src/main.rs
mod inventario;
use inventario::articulo::Articulo;
fn main() {
let existencias = vec![
Articulo::nuevo("tornillos", 1.50, 120),
Articulo::nuevo("tuercas", 0.80, 80),
];
for a in &existencias {
println!("{}: {:.2}", a.nombre, a.valor());
}
println!("total: {:.2}", inventario::valor_total(&existencias));
}
inventario.rs junto a la carpeta inventario/— es el estilo recomendado desde la edición 2018. El antiguo ponía el contenido del módulo en inventario/mod.rs y sigue funcionando, así que te lo encontrarás en proyectos con años. Lo que no conviene es mezclar los dos en el mismo proyecto.pub: todo es privado hasta que digas lo contrario
En Rust la visibilidad va al revés que en muchos lenguajes: nada se exporta por defecto. Un módulo puede ver lo que hay dentro de sus hijos solo si está marcado, y eso convierte la superficie pública en una decisión explícita en vez de un descuido.
Hay más de dos niveles, y el intermedio se usa mucho más de lo que parece:
| Marca | Quién lo ve |
|---|---|
| sin nada | Solo el módulo donde está, y sus hijos |
pub(crate) |
Todo tu crate, pero nada de fuera. Ideal para utilidades internas |
pub(super) |
El módulo padre |
pub |
Todo el mundo, incluido quien use tu biblioteca |
pub no hace públicos sus campos: cada campo necesita su propio pub, y por eso en el ejemplo de arriba unidades queda privado aunque Articulo sea público. Con los enums es justo al revés: si el enum es pub, todas sus variantes lo son automáticamente, porque un enum con variantes ocultas no serviría para nada.use: dejar de escribir rutas largas
use no importa nada ni ejecuta nada: solo crea un atajo para un camino del árbol de módulos. Los caminos pueden ser absolutos, desde la raíz del crate, o relativos:
use std::collections::{HashMap, HashSet};
use std::io::Write as _;
use std::fmt::Result as ResultadoFmt;
mod tienda {
pub mod almacen {
pub fn contar() -> u32 { 42 }
}
pub mod caja {
pub fn informe() -> u32 {
super::almacen::contar()
}
}
}
use crate::tienda::caja;
fn main() {
let mut m: HashMap<&str, u32> = HashMap::new();
m.insert("cajas", caja::informe());
let s: HashSet<u32> = m.values().copied().collect();
println!("{:?} {:?}", m.get("cajas"), s.len());
let _: Option<ResultadoFmt> = None;
}
Las llaves agrupan varios atajos del mismo camino. as renombra, que sirve cuando dos cosas se llaman igual. super:: sube al módulo padre, como .. en una ruta de carpetas, y crate:: arranca desde la raíz.
pub use: enseñar una fachada ordenada
Hay un uso de use que cambia el diseño de una biblioteca. Si lo marcas como público, además de crear el atajo para ti, lo expones para quien use tu crate:
mod interno {
pub mod muy {
pub mod profundo {
pub struct Cliente {
pub nombre: String,
}
}
}
}
pub use interno::muy::profundo::Cliente;
fn main() {
let c = Cliente { nombre: String::from("Ada") };
println!("{}", c.nombre);
}
Quien use la biblioteca escribe mi_crate::Cliente y no necesita saber que por dentro vive tres niveles más abajo. Eso te deja reorganizar las tripas sin romperle el código a nadie, y es la razón de que las bibliotecas buenas tengan una API plana aunque por dentro estén muy divididas.
Usar código de otros
Añadir una dependencia es una orden, y Cargo escribe la línea en Cargo.toml por ti:
$ cargo add rand [package] name = "mi_tienda" version = "0.1.0" edition = "2021" [dependencies] rand = "0.10.3"
Ese número no significa «exactamente esta versión». Por defecto Cargo lo interpreta como «esta o cualquiera posterior que sea compatible»: para 0.10.3 acepta hasta antes de 0.11.0, y para un 1.2.3 aceptaría hasta antes de 2.0.0. En las versiones 0.x el número del medio hace de versión mayor, porque se asume que el paquete todavía puede romper cosas.
Lo que de verdad se usó queda anotado en Cargo.lock, que Cargo genera y actualiza solo:
Cargo.lock, con cuidado. Durante años circuló la regla «los binarios sí, las bibliotecas no». La documentación actual de Cargo ya no lo plantea así: cargo new lo versiona por defecto y recomienda hacerlo cuando quieras compilaciones reproducibles, útiles para git bisect, para integración continua y para fijar la versión mínima de Rust. El matiz que importa es otro: tu Cargo.lock no afecta a quien consume tu paquete, solo lo hace tu Cargo.toml. Por eso en una biblioteca da una falsa sensación de control.Con la dependencia declarada, se usa como cualquier módulo:
use rand::RngExt;
fn main() {
let mut generador = rand::rng();
let tirada: u32 = generador.random_range(1..=6);
println!("salió un {tirada}");
}
Dónde van las pruebas
Rust pone las pruebas unitarias en el mismo archivo que el código, dentro de un módulo marcado para que solo se compile al probar. Suena raro y resulta muy cómodo, porque la prueba ve también lo privado:
pub fn con_descuento(precio: f64, porcentaje: f64) -> f64 {
precio - precio * porcentaje / 100.0
}
#[cfg(test)]
mod pruebas {
use super::*;
#[test]
fn aplica_el_descuento() {
assert_eq!(con_descuento(100.0, 20.0), 80.0);
}
#[test]
fn sin_descuento_no_cambia() {
assert_eq!(con_descuento(50.0, 0.0), 50.0);
}
}
El #[cfg(test)] hace que ese módulo no exista en la compilación normal, así que no engorda el binario que entregas. El use super::*; trae todo lo del módulo padre, que es lo que permite llamar a la función sin repetir su camino. Se ejecutan con cargo test.
En resumen
| Lo que quieres hacer | Qué usar |
|---|---|
| Empezar un programa | cargo new nombre |
| Empezar una biblioteca | cargo new --lib nombre |
| Comprobar rápido mientras escribes | cargo check |
| Partir el código en otro archivo | mod algo; y crear algo.rs |
| Meter submódulos dentro de uno | una carpeta algo/ junto a algo.rs |
| Exponer algo fuera del módulo | pub |
| Compartirlo solo dentro del crate | pub(crate) |
| Acortar un camino largo | use |
| Subir al módulo padre | super:: |
| Dar una API plana a tu biblioteca | pub use |
| Añadir una biblioteca de terceros | cargo add nombre |
| Escribir pruebas | un mod con #[cfg(test)] |
La idea que conviene llevarse es que el árbol de módulos de Rust es independiente del sistema de archivos: los archivos son solo el sitio donde se guarda el texto, y lo que el compilador ve es el árbol que tú declaras con mod. En cuanto se interioriza eso, los errores de ruta dejan de parecer caprichosos y se vuelven previsibles.
Para verlo explicado

«35.- Curso Rust. El Arte de Organizar el Código: Paquetes y Crates.», del canal Jesús Conde (15:12). Material de otro canal que recomendamos como complemento, no una producción de decodigo.com. El reproductor solo se carga al pulsar, así que YouTube no recibe nada tuyo hasta entonces. También puedes verlo en YouTube.