CommunityProgramación y desarrollogithub.com

tenequm/rust-dev

Guía del primer día para programar bien en Rust: ownership, errores como valores, String vs &str, Box/Rc/Arc, anyhow vs thiserror, y una lista breve de crates (tokio, serde, axum, sqlx). Úsalo al iniciar un proyecto Rust, luchar contra el borrow checker o elegir crates.

¿Qué es rust-dev?

Este skill proporciona una base práctica y fundamentada para escribir aplicaciones Rust de calidad desde el primer commit. No es un libro de texto, sino una guía enfocada en las diferencias clave con otros lenguajes, las decisiones del primer día que condicionan todo el desarrollo, y un conjunto reducido de crates que cubren la mayoría de las aplicaciones reales. Está diseñado para quienes se inician en Rust desde Python, JavaScript, Go, Java/C# o C++, y necesitan respuestas rápidas sobre ownership, borrowing, tipos de errores, punteros inteligentes y selección de crates. El resumen cubre los cinco pilares del modelo mental de Rust (ownership, aliasing XOR mutability, errores como valores, traits, y el borrow checker como oráculo de diseño), una tabla de decisiones para el día 1, modismos esenciales, y una lista corta de crates recomendados como serde, tokio, anyhow, thiserror, clap y axum.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Desarrollo en Rust - Día 1

Una base práctica para escribir aplicaciones Rust de calidad desde el primer commit. No es un libro de texto. Se centra en las diferencias con otros lenguajes, las decisiones del primer día que condicionan todo lo demás, y el pequeño conjunto de crates que cubren la mayoría de las aplicaciones reales.

Cuándo usarlo

  • Al iniciar un nuevo proyecto Rust (CLI, servicio, biblioteca)
  • Al venir a Rust desde Python, JavaScript, Go, Java/C# o C++
  • Al elegir entre tipos owned/borrowed, punteros inteligentes, trait objects vs genéricos
  • Al seleccionar una estrategia de manejo de errores (anyhow vs thiserror)
  • Al decidir qué crates utilizar
  • Al configurar un Cargo.toml mínimo pero con opiniones, clippy y rustfmt

Configuración del Día 1

# 1. Instalar el toolchain (rustup es el gestor de toolchains)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. Confirmar componentes (rustfmt y clippy vienen con stable, rust-src habilita funciones del IDE)
rustup component add rustfmt clippy rust-src

# 3. Crear un proyecto
cargo new my-app          # binario (src/main.rs)
cargo new --lib my-lib    # biblioteca (src/lib.rs)

# 4. El ciclo de desarrollo (memoriza estos cuatro)
cargo check     # verificación rápida de tipos, sin generación de código
cargo run       # compilar y ejecutar (binario)
cargo test      # compilar y ejecutar pruebas (incl. doctests)
cargo clippy --all-targets   # lint, incl. tests/ y benches/ (ejecutar antes de subir)
cargo fmt       # formatear

# 5. Gestionar dependencias sin editar Cargo.toml a mano
cargo add tokio --features full
cargo remove tokio
cargo update                 # recalcular Cargo.lock dentro de los rangos semver existentes

cargo update solo se mueve dentro de los rangos de versiones ya presentes en Cargo.toml. Cruzar una versión mayor (1.x a 2.0) requiere editar Cargo.toml o usar cargo add <crate>@2.

rust-analyzer es obligatorio. Es el servidor de lenguaje que usa todo editor (VS Code, Zed, Neovim, Helix, RustRover usa su propio motor pero es comparable). En VS Code, instala la extensión rust-analyzer y configura rust-analyzer.check.command como "clippy" para recibir feedback de lint al guardar.

¿Quieres un vigilante de archivos más adelante? cargo install --locked bacon, luego ejecuta bacon en tu proyecto. No es necesario el día 1.

El Modelo Mental de Rust en 5 Ideas

Rust cambia dos cosas que das por sentadas en la mayoría de los lenguajes (un recolector de basura y las excepciones) por garantías en tiempo de compilación sobre memoria, condiciones de carrera y manejo de errores. La forma del lenguaje se deriva de ese intercambio.

1. Ownership: cada valor tiene exactamente un dueño

Piensa en los valores como objetos físicos. Un libro, un archivo, una conexión de red. En cualquier momento, una variable es dueña de él. Puedes:

  • Moverlo: let b = a; transfiere la propiedad a b. a ya no existe.
  • Prestarlo inmutablemente: &a permite que otros lo miren. Muchos lectores permitidos.
  • Prestarlo mutablemente: &mut a permite que una persona lo modifique. Acceso exclusivo.
  • Clonarlo: a.clone() hace una copia profunda. Ambos conservan la suya.

Cuando el dueño sale del ámbito, el valor se elimina (memoria liberada, archivo cerrado, bloqueo liberado). Sin GC, sin free manual. Esto es RAII, exigido por el compilador.

2. Aliasing XOR mutabilidad

En cualquier momento, un dato tiene o:

  • una referencia mutable (&mut T), o
  • cualquier número de referencias inmutables (&T),

nunca ambas. Esta única regla elimina las condiciones de carrera y la mayoría de los errores de uso después de liberar. El borrow checker la exige. Cuando se queja, te está diciendo que tu historia de propiedad de datos no es clara, no que el lenguaje sea difícil.

3. Los errores son valores, no excepciones

No hay try/catch. Las funciones que pueden fallar devuelven Result<T, E>. Las funciones que pueden no devolver nada útil devuelven Option<T>. El compilador te obliga a manejar ambos. El operador ? propaga errores hacia arriba en la pila de llamadas con un solo carácter:

fn read_config() -> Result<Config, anyhow::Error> {
    let text = std::fs::read_to_string("config.toml")?;  // ? = retorno temprano en Err
    let config = toml::from_str(&text)?;
    Ok(config)
}

No existe null. Option<T> es None o Some(value). El compilador no te dejará olvidar el caso None.

4. Los traits no son interfaces de Java

Un trait define comportamiento. Los tipos implementan (impl) traits. Hasta aquí familiar. Las diferencias:

  • El dispatch estático es el predeterminado. Cuando escribes fn f<T: Display>(x: T), el compilador genera una copia separada de f para cada T concreto con el que la llames (monomorfización, como las plantillas de C++). Sin sobrecarga en tiempo de ejecución.
  • El dispatch dinámico es opt-in mediante dyn Trait (normalmente Box<dyn Trait> o &dyn Trait). Una búsqueda en vtable por llamada.
  • Sin herencia. Los traits se componen. Si te encuentras usando Deref para "extender" un tipo, detente y usa composición o un enum.
  • Regla del huérfano: puedes implementar TuTrait para TipoDeOtro o TraitDeOtro para TuTipo, pero no ambos foráneos. Esto mantiene la resolución de dependencias sensata.

5. El borrow checker es un oráculo de diseño

El error más común de los principiantes es tratar los errores del compilador como obstáculos que silenciar. No lo son. Casi cada error de borrow-check revela un problema real sobre quién es dueño de qué. Cuando te atasques, la pregunta rara vez es "¿cómo hago que esto compile?" y casi siempre "¿cuál es la relación de propiedad real que quiero aquí?". Lee el error. El compilador es inusualmente informativo.

Las 3 Preguntas para Cada Firma de Función

Antes de escribir una función, pregúntate: ¿necesita ser dueña, leer o modificar la entrada?

fn consume(s: String)        // es dueña:    la función toma responsabilidad, la llamante la pierde
fn read(s: &str)             // lee:         la función lo mira, la llamante lo conserva
fn modify(s: &mut String)    // modifica:    la función lo cambia en el lugar

Valores predeterminados que funcionan el 90% del tiempo:

  • Parámetros de función: prefiere &str sobre String, &[T] sobre Vec<T> (son slices, aceptan tanto llamantes owned como borrowed).
  • Retornos de función: devuelve tipos owned (String, Vec<T>). Devolver referencias implica lifetimes; evítalos hasta que los necesites.
  • Campos de struct: prefiere tipos owned (String, Vec<T>). Almacenar &str en un struct es la trampa más común para principiantes y propaga anotaciones de lifetime por cada tipo que contenga tu struct.

Tabla de Decisiones del Día 1

Respuestas de una línea para las elecciones que surgen primero.

DecisiónPredeterminadoCuándo elegir el otro
String vs &str (campo de struct)StringCasi nunca &str hasta que tengas una razón real y entiendas los lifetimes
String vs &str (parámetro de función)&strUsa String solo si debes ser dueño/almacenarlo internamente
Vec<T> vs &[T] (parámetro)&[T]Vec<T> solo si debes ser dueño
Box<T> vs Rc<T> vs Arc<T>Box<T> (único dueño, heap)Arc<T> para propiedad compartida entre hilos. Evita Rc<T> como predeterminado; usa Arc<T> para no refactorizar cuando vayas a async
RefCell<T> vs Mutex<T>Mutex<T> (o RwLock<T>)Misma razón: funciona en async/hilos, mientras que RefCell no
Option<T> vs Result<T, E>Option<T> para "sin valor", Result<T, E> para "falló por una razón"Si la ausencia tiene un significado que la llamante debe manejar, Result
dyn Trait vs impl Trait / <T: Trait>Genérico (<T: Trait> o impl Trait) - dispatch estáticoBox<dyn Trait> cuando necesites una colección heterogénea (Vec<Box<dyn Animal>>)
Errores en código de aplicaciónanyhow::Result<T> en todas partes-
Errores en código de bibliotecaEnum derivado de thiserrorNunca Box<dyn Error> en APIs públicas de bibliotecas - obliga a las llamantes a hacer downcast
&self vs &mut self vs self&self para getters, &mut self para setters, self para builders/operaciones consumidoras-
Diseño de módulosMódulos en línea hasta que un archivo se alargue, luego dividirUn módulo = un archivo es un instinto de Java/C#, no de Rust

Modismos para Interiorizar Temprano

Estos aparecen en casi todos los programas Rust. Apréndelos en la primera semana.

? para propagación de errores. Reemplaza nueve líneas de match con un solo carácter.

let body = reqwest::get(url).await?.text().await?;

Cadenas de iteradores sobre bucles manuales. Compilan al mismo código máquina que los bucles escritos a mano (LLVM inlinea closures). El Rust idiomático es funcional en estilo.

let active_emails: Vec<String> = users
    .iter()
    .filter(|u| u.active)
    .map(|u| u.email.clone())
    .collect();

Exhaustividad de match. Añade una nueva variante a un enum y cada match que no la maneje se convierte en un error de compilación. Úsalo. Es una de las herramientas de refactorización más poderosas en cualquier lenguaje.

Deja que el compilador guíe las refactorizaciones. El mismo truco funciona con structs: añade un campo obligatorio sin Default y cada constructor existente se convierte en un error de compilación: una lista de trabajo exhaustiva y gratuita de todos los sitios a actualizar.

if let y let else para el caso común de un solo brazo.

if let Some(name) = user.name { println!("hi {name}"); }

let Some(name) = user.name else { return Err(anyhow!("no name")); };
// `name` está en ámbito de aquí en adelante, sin anidamiento

From / Into para conversiones de tipos. Implementa From, obtén Into gratis. ? usa From para convertir tipos de error automáticamente.

Combinadores en Option / Result. Recurre a .map, .and_then, .unwrap_or, .unwrap_or_else, .ok_or antes de recurrir a match.

Macros de derive. #[derive(Debug, Clone, PartialEq)] te da el 80% del código repetitivo gratis. Añade #[derive(Serialize, Deserialize)] para JSON.

Azúcar reciente. Características estabilizadas que vale la pena conocer, de más nuevas a más antiguas: bool::ok_or y ok_or_else (1.98) convierten una condición directamente en un Result (ready.ok_or(Error::NotReady)?). cfg_select! es un match en tiempo de compilación sobre predicados cfg, que reemplaza la mayoría de los usos del crate cfg-if, y los guards if let funcionan en brazos de match (match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... }) - ambos en 1.95. assert_matches! y debug_assert_matches! (1.96) afirman sobre un patrón en lugar de igualdad, que es la afirmación natural para un enum. Dos más antiguos que verás constantemente y no debes confundir con exóticos: let-chains (if let Some(x) = a && x > 3 { ... }), estable en edition 2024 desde 1.88, y closures asíncronos (async || { ... }, con la familia de bounds AsyncFn), estable desde 1.85.

Viniendo de X, Aquí lo que te Muerde

Desde Python o JavaScript:

  • let b = a; para un valor en el heap (como String, Vec) lo mueve. a ya no es utilizable. Usa &a para prestar o a.clone() para copiar.
  • Sin null. Option<T> es obligatorio.
  • Sin excepciones. Result<T, E> y ?. El compilador no te dejará ignorar errores.
  • Sin herencia. Composición + traits + enums.
  • Las variables son inmutables por defecto. Añade mut para mutar. Igual para referencias: & vs &mut.
  • Volver a declarar un nombre está permitido y es idiomático: let input = input.trim(); sombrea el antiguo binding con uno nuevo, posiblemente de un tipo diferente. No es mutación, y es cómo el código Rust convierte un valor en el lugar sin inventar input2.
  • Los tipos enteros son explícitos y la indexación requiere usize. También desbordan de manera diferente según la compilación: debug "incluye comprobaciones de desbordamiento de enteros que hacen que tu programa entre en pánico en tiempo de ejecución", mientras que --release las elimina y realiza "wrapping de complemento a dos". Un conjunto de pruebas que pasa puede aún así hacer wrapping en producción, así que usa checked_*/saturating_*/wrapping_* donde la aritmética pueda realmente alcanzar el límite, en lugar de confiar en el pánico de debug para encontrarlo. strict_add y similares (estables desde 1.91) entran en pánico por desbordamiento en toda compilación, y overflow-checks = true bajo [profile.release] mantiene el comportamiento de debug en producción. Y as entre tipos enteros trunca silenciosamente (300_u32 as u8 es 44) - usa u8::try_from(x)? donde el valor pueda no caber.
  • Un String no es indexable. s[0] no compila - consulta references/ownership-and-types.md para ver qué usar en su lugar.

Desde Go:

  • Errores como valores - mismo instinto, pero usa ? en lugar de if err != nil.
  • Sin nil. Option<T>.
  • Sin GC y sin gorutinas: ownership + borrowing, async/await con tokio. El modelo async es cooperativo (await es un punto de rendición explícito), no preventivo.
  • interface{} se convierte en traits. Por defecto, genéricos para dispatch estático; Box<dyn Trait> solo cuando lo necesites.
  • El enlace estático es el predeterminado. Los binarios son más grandes pero autocontenidos.
  • panic! debe reservarse para bugs irrecuperables en código de aplicación; no lo uses como el "log y continúa" de Go.

Desde Java o C#:

  • Los traits no son interfaces con dispatch virtual por defecto. <T: Trait> se monomorfiza. dyn Trait es la versión dinámica opt-in.
  • Sin referencias nulas. Option<T>.
  • Sin excepciones. Result<T, E> y ?.
  • Sin herencia de clases. Usa enums para tipos suma, traits para comportamiento compartido.
  • Sin GC: ownership y borrowing deciden los lifetimes. Arc<T> es lo más parecido a una referencia de Java.
  • Los genéricos se monomorfizan, no se borran.

Desde C++:

  • Como RAII, pero el borrow checker lo exige en tiempo de compilación.
  • Sin constructores de copia/movimiento. Clone es explícito y Copy es un trait marcador para copias bit a bit baratas.
  • Sin comportamiento indefinido en código seguro (en teoría).
  • & es un préstamo verificado en tiempo de compilación, no un puntero crudo. Los punteros crudos existen (*const T, *mut T) pero requieren unsafe para desreferenciar.
  • Los punteros inteligentes son Box<T> (unique_ptr), Rc<T> (shared_ptr, un solo hilo), Arc<T> (shared_ptr, seguro para hilos).
  • Las macros son higiénicas. Las macros procedurales (derive, atributo, similares a funciones) son cómo funcionan serde, tokio::main, etc.

La Lista Corta de Crates

Estos cubren la mayoría de las aplicaciones reales. Añádelos según sea necesario; no todos son obligatorios.

CrateQué te da
serde + serde_jsonSerialización. #[derive(Serialize, Deserialize)] y ya está
tokioRuntime asíncrono. #[tokio::main], tokio::spawn, E/S asíncrona
anyhowTipo de error para aplicaciones. anyhow::Result<T>, bail!, context()
thiserrorEnums de error para bibliotecas. #[derive(thiserror::Error)]
clapAnálisis de argumentos de CLI. #[derive(Parser)], genera completado automático y --help
axumServidor web (capa sobre tokio + tower). Maneja async, extracte, middlewares
sqlxAcceso a bases de datos. Compila consultas, con tipos, soporte para PostgreSQL, MySQL, SQLite
reqwestCliente HTTP. reqwest::get(url).await?.text().await?
tracingRegistro y traza estructurados. info!("listening on {port}"), con spans, reemplaza log
chronoFechas y horas. Utc::now(), Duration, zonas horarias
uuidUUIDs. Uuid::new_v4()
randNúmeros aleatorios. thread_rng().gen_range(0..100)
regexExpresiones regulares. Se compilan una vez por ejecución
tempfileArchivos y directorios temporales. tempfile::NamedTempFile::new()?
criterionBenchmarks (en benches/). Se ejecuta con cargo bench
testcontainersDependencias de prueba (PostgreSQL, Redis, etc. con Docker). Inicia/limpia automáticamente
instaPruebas de snapshot. Verifica que la salida de funciones coincida con un archivo snapshot aprobado

Herramientas de línea de comandos

Instaladas globalmente con cargo install; no las añadas como dependencias de proyecto.

HerramientaPropósito
cargo-watchRe-ejecuta cargo check/cargo test al guardar archivos
baconRe-ejecuta cargo test al guardar, con colas de trabajos y diseño mejorado (cargo install --locked bacon)
cargo-auditBusca CVEs conocidos en Cargo.lock
cargo-denyAplica políticas de licencias y dependencias
cargo-outdatedMuestra dependencias que tienen versiones más recientes
cargo-msrvDetermina la versión mínima de Rust que puede compilar tu proyecto
cargo-expandExpande las macros y muestra el código resultante (para depurar derive macros)

Qué Evitar el Día 1 / Elecciones de Arquitectura

Librerías vs Aplicaciones

La regla es clara: usa thiserror en bibliotecas, anyhow en aplicaciones.

// biblioteca (público): enum de error tipado
#[derive(thiserror::Error)]
pub enum ParseError {
    #[error("se esperaban {expected} bytes, se obtuvieron {got}")]
    LengthMismatch { expected: usize, got: usize },
    #[error("código de estado inválido: {0}")]
    InvalidStatus(u8),
}
// aplicación (binario): anyhow envuelve cualquier error
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let data = std::fs::read("input.bin").context("no se pudo leer el archivo de entrada")?;
    let parsed = parse_frame(&data).context("el archivo no tiene un formato de trama válido")?;
    Ok(())
}

Una biblioteca que devuelve anyhow::Error obliga a cada usuario a manejar una caja negra. Una biblioteca que devuelve thiserror::Error permite a los usuarios emparejar variantes con match, propagar con ?, o envolver con anyhow. En el otro sentido, una aplicación que usa thiserror para cada error interno añade ruido: el tipo exacto rara vez importa más allá de su mensaje y contexto. anyhow::Error es una caja de Box<dyn Error + Send + Sync> con un backtrace adjunto, suficiente para aplicaciones.

dyn Trait vs Genéricos (impl Trait o <T: Trait>)

La mayoría de los nuevos en Rust alcanzan Box<dyn Trait> por familiaridad con interfaces de otros lenguajes. Suele estar mal. El dispatch estático (genéricos / impl Trait) es el predeterminado correcto:

  • La función se monomorfiza: sin búsqueda en vtable, sin asignación en heap.
  • El compilador puede inlinear en todos los sitios de llamada.
  • El único coste es el tamaño del binario (bloat de código), que rara vez importa.

Necesitas dyn Trait solo cuando:

  • Colecciones heterogéneas: Vec<Box<dyn Animal>> donde cada elemento es un tipo diferente que implementa Animal.
  • Reducir el tamaño de tipos grandes: un enum de tales con diez variantes puede ser enorme si cada una contiene un genérico diferente. Box<dyn Trait> lo reduce a un puntero.
  • Módulos dinámicamente cargados (plugins, FFI).

Arc<Mutex<T>> es el Nuevo Box<dyn Trait>

Cuando vengas de un lenguaje con un recolector de basura y referencias compartidas y mutables por todas partes, Arc<Mutex<T>> se siente como la respuesta a todo. No lo es. Pregunta primero: ¿es el árbol de llamadas secuencial? Si solo es cuestión de cuándo se accede, no dónde, entonces &mut T prestado funciona en un contexto sincrónico, y las variantes de ejecución única de tokio (por ejemplo, tokio::task::LocalSet) permiten RefCell a través de puntos await. Arc<Mutex<T>> introduce contención real de bloqueo, puntos muertos potenciales y complejidad de inferencia de tipos cuando se mezcla con closures y async. Resérvalo para el estado compartido que cruza hilos reales (incluyendo tareas tokio enviadas con spawn).

Para estado compartido dentro de una sola tarea tokio (que se ejecuta en un solo hilo a la vez), tokio::sync::Mutex es mejor que std::sync::Mutex (abandona el bloqueo en await, no bloquea el hilo). Pero el mejor enfoque suele estar libre de bloqueos por completo: pasa los datos como &mut a través de los puntos await, no los envuelvas en un Mutex.

Cómo Empezar: cargo init + mi Cargo.toml

[package]
name = "my-app"
version = "0.1.0"
edition = "2024"

[dependencies]
# App
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

# Añadir cuando sea necesario:
# clap = { version = "4", features = ["derive"] }
# axum = "0.8"
# sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
# reqwest = { version = "0.12", features = ["json"] }
# tracing = "0.1"
# tracing-subscriber = "0.3"

[dev-dependencies]
# Los marcos de prueba recomendados no son crates; las pruebas de snapshot y los containers son
# opcionales y normalmente se añaden proyecto por proyecto.

[profile.release]
# Opcional: mantener comprobaciones de desbordamiento incluso en compilaciones release.
# overflow-checks = true

[lints.clippy]
# Opcional: habilitar lints adicionales de clippy
# cargo = { level = "warn", priority = -1 }
# pedantic = { level = "warn", priority = -1 }

Configuraciones del Editor / Lint

rustfmt

# rustfmt.toml
max_width = 100
# Prefiere la forma "a la izquierda" de Rust estándar (a la derecha de `=`, a la izquierda de la línea)
use_small_heuristics = "Max"
# Mantén las cadenas de iteradores cortas; no dividas cada paso
chain_one_line_max = 80

.clippy.toml

msrv = "1.85"  # o tu msrv real; clippy no sugerirá características más nuevas

Depuración

  • dbg!() es la forma más rápida: dbg!(x) imprime [src/main.rs:10] x = 42 y devuelve la propiedad de x. Úsalo para depuración rápida, no lo subas.
  • eprintln! va a stderr, no stdout. Úsalo para errores y registros de depuración (aunque tracing es mejor para producción).
  • Depuradores: rust-gdb y rust-lldb envuelven gdb/lldb con scripts de pretty-printing de Rust. cargo run -- foo pasa foo como argumento a tu binario. cargo run -- --bar pasa --bar.
  • Los backtraces de pánico requieren RUST_BACKTRACE=1. Para backtraces completos con números de línea: RUST_BACKTRACE=full. Establece esto en tu perfil de shell o archivo .env.
  • cargo-expand (herramienta de línea de comandos) expande macros para que puedas ver el código real que genera #[derive(...)].
  • "Rust Analyzer: Open Docs" en un símbolo abre la documentación.
  • "Rust Analyzer: Show References" encuentra todos los usos.
  • cargo doc --open compila la documentación de tu proyecto y todas las dependencias y la abre en el navegador.

Advertencia sobre la Documentación

La documentación de Rust (std docs, cargo doc, docs.rs) es consistentemente la mejor de cualquier ecosistema que hayas usado. Está enlazada desde cada error del compilador, cada crate publica en docs.rs, cada función muestra su firma, ejemplos y trait bounds. No uses StackOverflow ni ChatGPT como primera opción. Lee la documentación primero. Te ahorrará horas.

¿Y rustc --explain E0277? Pega el código de error y el compilador te dará una explicación detallada con ejemplos. Pruébalo.


La guía del día 1 termina aquí. Si tienes dudas sobre escenarios específicos (async, rendimiento, FFI), pregunta. Pero este material por sí solo hará que tu primer proyecto Rust sea drásticamente más suave que sin él.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Skills relacionados