Guía completa de Rust para desarrolladores: propiedad, concurrencia y rendimiento
Esta guía práctica reúne conceptos clave, patrones y ejemplos completos para escribir código Rust seguro y rápido. Cobertura: propiedad y borrow checker, lifetimes, manejo de errores, async/concurrency, optimización de memoria, unsafe/FFI, herramientas y estructura de proyecto. Código completo y explicación del por qué en cada sección.
Estructura del proyecto (recomendada)
mi_proyecto_rust/
├─ Cargo.toml
├─ src/
│ ├─ main.rs // binario
│ ├─ lib.rs // lógica reutilizable y tests
│ ├─ ownership.rs // ejemplos de propiedad y borrow
│ ├─ errors.rs // manejo de errores
│ ├─ async_mod.rs // ejemplos async con tokio
│ └─ ffi.rs // ejemplo mínimo FFI
├─ benches/ // benchmarks (criterion)
└─ Cargo.lock
Separar la lógica en lib.rs facilita testing y reuso. Mantén main.rs para orquestar.
1) Propiedad, préstamos y lifetimes (por qué importa)
Rust elimina gran parte de las clases de errores relacionados con memoria en tiempo de compilación. Comprender ownership/borrowing evita clones innecesarios y operaciones costosas.
Ejemplo: mover vs referenciar
// src/ownership.rs
pub fn move_example() {
let s = String::from("hola");
let s2 = s; // move: s ya no es válido
// println!("{}", s); // error de compilación
println!("{}", s2);
}
pub fn borrow_example() {
let s = String::from("hola");
let len = calculate_len(&s); // préstamo inmutable
println!("longitud: {}", len);
}
fn calculate_len(s: &String) -> usize {
s.len()
}
Por qué: usar préstamos evita clones y copias. Prefiere referencias inmutables cuando solo consultas datos.
Lifetimes básicos
// caso simple de lifetimes en src/ownership.rs
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Uso:
// let r;
// {
// let s1 = String::from("longitud larga");
// let s2 = "corta";
// r = longest(s1.as_str(), s2);
// }
// println!("{}", r); // error si r viviera más que s1
Por qué: declarar lifetimes hace explícita la relación de alcance entre referencias y evita dangling pointers.
2) Manejo de errores: Result, thiserror y anyhow
Usa Result y enum de errores para API pública (detallados). Para aplicaciones CLI, anyhow y Context agilizan el manejo. Para librerías, define errores con thiserror.
// src/errors.rs
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum AppError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Parsing error: {0}")]
Parse(String),
}
pub fn might_fail(path: &str) -> Result {
let s = std::fs::read_to_string(path)?; // convierte io::Error -> AppError::Io
if s.is_empty() { Err(AppError::Parse("vacío".into())) } else { Ok(s) }
}
Por qué: modelos explícitos de error facilitan pruebas y manejo en capas superiores. Evita unwrap() en producción.
3) Async en Rust (Tokio)
Para I/O concurrido, usa Tokio + async/await. Evita bloqueos (blocking) en runtimes async.
// src/async_mod.rs
use tokio::time::{sleep, Duration};
pub async fn worker(id: usize) {
println!("Worker {} iniciando", id);
sleep(Duration::from_millis(100 * id as u64)).await;
println!("Worker {} terminado", id);
}
pub async fn run_concurrent() {
let handles: Vec<_> = (1..=5).map(|i| tokio::spawn(worker(i))).collect();
for h in handles {
let _ = h.await; // propaga posibles panics del task
}
}
Consejo: utiliza tokio::spawn para tareas CPU-limitadas delega a spawn_blocking o thread-pools.
4) Concurrencia segura: Send y Sync
Tipos que cruzan threads deben implementar Send. Acceso concurrente a datos mutables requiere primitivas: Mutex, RwLock o canales (mpsc).
use std::sync::{Arc, Mutex};
let data = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..4).map(|_| {
let d = Arc::clone(&data);
std::thread::spawn(move || {
let mut v = d.lock().unwrap();
*v += 1;
})
}).collect();
Por qué: combinar Arc (conteo atómico) y Mutex es el patrón estándar para estados compartidos entre hilos.
5) Optimización de memoria y rendimiento
Pequeños cambios marcan diferencia: evitar clones, reservar capacidad, preferir iteradores, medir antes de optimizar.
- Usa
Vec::with_capacitycuando conozcas tamaño aproximado. - Evita
String<->&strconversions innecesarias. - Prefiere
iter()ycollect()en lugar de loops manuales cuando el optimizador puede vectorizar. - Comprende layout:
repr(C)para FFI, agrupación de campos para evitar padding.
// ejemplo: evitar clones
fn join_words(words: Vec) -> String {
words.join(" ") // consume el vector
}
Herramientas: cargo bench (criterion), perf + flamegraph, cargo-llvm-lines para ver hot spots.
6) Uso de unsafe: cuándo y cómo
No uses unsafe a la ligera. Motivos válidos: FFI, optimizaciones críticas, acceder a APIs de bajo nivel. Encapsula unsafe detrás de APIs seguras y prueba rigurosamente.
// ejemplo mínimo: leer memoria sin inicializar (solo demo)
unsafe fn read_uninit() -> i32 {
use std::mem::MaybeUninit;
let mut x: MaybeUninit = MaybeUninit::uninit();
// inicializar manualmente
x.as_mut_ptr().write(42);
x.assume_init()
}
Reglas: documenta invariantes, limita el alcance de bloques unsafe, revisa con colegas.
7) FFI con C (ejemplo mínimo)
// src/ffi.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// Para usar desde C: genera cdylib y llama add
En Cargo.toml usa crate-type = ["cdylib"] para crear una librería enlazable.
8) Pruebas y benchmarks
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2,3), 5);
}
}
Para micro-benchmarks usa criterion (más fiable que bench estable). Integra en CI para detectar regresiones.
9) Herramientas útiles
rustfmt: formato consistenteclippy: recomendaciones idiomáticascargo-audit: detecta vulnerabilidades de dependenciascargo-deny: políticas de licencia y vetadorust-analyzer: autocompletado y navegación
10) CI básica con GitHub Actions
name: Rust CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
- name: Cache cargo
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Run tests
run: cargo test --all
- name: Run clippy
run: cargo clippy --all -- -D warnings
Checklist de mejores prácticas
- Evita unwrap() en librerías: propaga errores
- Prefiere referencias y borrows sobre clones
- Usa crates maduras (tokio, serde, anyhow, thiserror)
- Encapsula unsafe y documenta invariantes
- Escribe tests unitarios y de integración
- Mide antes de optimizar: bench + profiler
Ejemplo completo mínimo: CLI que lee un archivo y procesa líneas async
// Cargo.toml (extracto)
// [package]
// name = "mi_proyecto_rust"
// edition = "2021"
//
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// anyhow = "1"
// thiserror = "1"
// src/main.rs
use anyhow::Context;
mod async_mod;
mod errors;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let path = std::env::args().nth(1).context("falta ruta al archivo")?;
let s = tokio::fs::read_to_string(&path).await.context("leer archivo")?;
for line in s.lines() {
tokio::spawn(async move {
// proceso por línea (simulado)
println!("procesando: {}", line);
});
}
// espera simple (en real usa JoinHandle o un contador)
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
Ok(())
}
Por qué: ejemplo ilustra lectura async + spawn de tareas. En producción recoge los JoinHandle para propagar errores o usa FuturesUnordered para control fino.
Recursos y siguientes pasos
- Libro oficial: The Rust Programming Language (disponible gratis)
- Rust By Example
- Repositorios de proyectos reales para estudiar patterns
Advertencia avanzada: no confíes solo en compilación exitosa para garantizar corrección en concurrencia y unsafe. Usa herramientas dinámicas (sanitizers), pruebas de propiedad (proptest) y revisiones de código para invariantes críticas.
Siguiente paso sugerido: crea un micro-servicio en Rust que exponga una API HTTP (axum/hyper), instrumenta con metrics + tracing y perfila con flamegraph para identificar cuellos de botella.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación