A progressive tour of Rust — ownership, borrowing, traits, and cargo. Each section builds on the ones before it, and every example is annotated with the gotcha that matters.
Hello World
Entry point is fn main(). println! is a
macro (note the !) — it expands at compile
time, it's not a regular function.
1fn main() {2 println!("Hello, world!");3}
Variables & Mutability
Bindings are immutable by default. Add
mut to allow change. Shadowing lets you
reuse a name — even with a new type.
1let x = 5; // immutable by default2let mut y = 5; // explicitly mutable3let z: i32 = 10; // type annotation4const MAX: u32 = 100; // compile-time constant56let s = "hi"; // &str7let s = s.len(); // shadowing: same name, new type89let (a, b) = (1, 2); // destructuring bind
Data Types
Scalars, fixed compound types, and the two string kinds:
&str (borrowed slice) vs String (heap-owned,
growable).
1// scalars2let a: i32 = -5;3let b: u64 = 5;4let pi: f64 = 3.14;5let flag: bool = true;6let heart: char = 'x'; // 4-byte unicode scalar78// compound9let pair: (i32, &str) = (1, "a");10let arr: [i32; 3] = [1, 2, 3]; // fixed size, stack1112// two string flavors13let borrowed: &str = "slice"; // borrowed, static14let owned: String = String::from("heap"); // growable
Control Flow
if is an expression. loop can
return a value via break. Ranges:
.. exclusive, ..= inclusive.
1if x > 5 {2 println!("big");3} else if x == 5 {4 println!("five");5} else {6 println!("small");7}89for i in 0..10 { } // 0..9 (exclusive)10for i in 0..=10 { } // 0..10 (inclusive)11while cond { }1213let n: i32 = loop {14 break 42; // loops return a value15};
Ownership
Every value has one owner; when the owner leaves scope
the value is dropped. Assignment moves non-Copy types.
Primitives implement Copy.
1let s1 = String::from("hi");2let s2 = s1; // MOVE: s1 is now invalid3// println!("{}", s1); // ERROR: use after move45// primitives implement Copy6let a = 5;7let b = a; // copied, a still valid8println!("{a} {b}"); // fine910// explicit deep copy11let s3 = s2.clone();
Borrowing & References
References let you use a value without taking ownership. The borrow
checker enforces: many shared &T
XOR one mutable &mut T
— never both at once.
1fn length(s: &String) -> usize { // borrows, no move2 s.len()3}45let mut s = String::from("hi");6let r1 = &s; // shared borrow (read-only)7let r2 = &s; // many shared: OK together8let r3 = &mut s; // ERROR: can't mix &mut with &910// slices borrow a contiguous view11let v = vec![1, 2, 3, 4];12let part: &[i32] = &v[1..3]; // [2, 3]
Structs & Enums
Structs group named fields; enums are tagged unions (variants can carry
data). impl blocks attach methods. Self = the
type itself.
1struct User { name: String, age: u32 }23impl User {4 fn new(name: &str) -> Self {5 User { name: name.to_string(), age: 0 }6 }7 fn birthday(&mut self) { self.age += 1; }8}910enum Shape {11 Circle(f64),12 Square(f64),13 Rect { w: f64, h: f64 },14}1516let u = User::new("ada");17let s = Shape::Circle(2.0);
Pattern Matching
match is exhaustive and total. Option<T>
replaces null — you must handle the missing case.
1fn area(s: Shape) -> f64 {2 match s {3 Shape::Circle(r) => 3.14159 * r * r,4 Shape::Square(side) => side * side,5 Shape::Rect { w, h } => w * h,6 }7}89fn first(xs: &[i32]) -> Option<i32> {10 if xs.is_empty() { None } else { Some(xs[0]) }11}1213match first(&[]) {14 Some(x) => println!("first = {x}"),15 None => println!("empty"),16}
Error Handling
No exceptions, no nulls. Failures are Result<T, E>.
The ? operator short-circuits and propagates Err
upward.
1use std::num::ParseIntError;23fn parse(s: &str) -> Result<i32, ParseIntError> {4 s.parse()5}67// ? propagates errors to the caller8fn double(s: &str) -> Result<i32, ParseIntError> {9 let n: i32 = parse(s)?; // return Err on failure10 Ok(n * 2)11}1213match double("21") {14 Ok(v) => println!("v = {v}"),15 Err(e) => println!("error: {e}"),16}
Traits & Generics
Traits are interfaces. Generics + trait bounds
monomorphize at compile time — zero-cost abstraction, no
vtable unless you use dyn.
1trait Greet {2 fn hello(&self) -> String;3}45struct User { name: String }67impl Greet for User {8 fn hello(&self) -> String {9 format!("hi, {}", self.name)10 }11}1213// static dispatch: T is specialized at compile time14fn max<T: PartialOrd>(a: T, b: T) -> T {15 if a > b { a } else { b }16}1718// dynamic dispatch via a trait object19fn greet(g: &dyn Greet) { println!("{}", g.hello()); }
Cargo
Cargo is the build system + package manager. Cargo.toml
declares deps; src/main.rs is the binary entry.
1$ cargo new myproj # scaffold a binary project2$ cargo build # compile (debug)3$ cargo build --release # optimized build4$ cargo run # build + run5$ cargo test # run #[test] functions6$ cargo add serde # add a dependency7$ cargo fmt # format the code8$ cargo clippy # lint
Modules & Imports
Code is organised with mod. use brings paths
into scope; pub makes items visible outside their module.
Paths are absolute from the crate root or relative to the current
module.
1// src/main.rs is the crate root of a binary2mod network; // loads src/network.rs (or network/mod.rs)3mod parser {4 pub fn parse(s: &str) -> u32 { 0 }5 fn internal() {} // private by default6}78// bring items into scope9use parser::parse; // single item10use std::collections::{HashMap, BTreeMap}; // multiple11use std::io as io2; // rename to avoid clash1213// re-export from a module14pub use parser::parse as parse_u32;1516// path qualifiers17// crate:: — crate root (absolute)18// self:: — current module19// super:: — parent module20use crate::network::connect;21use super::parser::parse as p;
Closures & Iterators
Closures capture their environment; annotate with Fn (by
ref), FnMut (by mut ref), FnOnce (by value).
Iterators are lazy — nothing runs until you collect or
consume.
1// closures2let add = |a: i32, b: i32| a + b; // inferred types3let n = 10;4let closure = move || n * 2; // move captures n by value56// iterator adaptors — lazy, chain freely7let v = vec![1, 2, 3, 4, 5];8let doubled: Vec<i32> = v.iter()9 .map(|x| x * 2) // transform10 .filter(|x| *x > 4) // keep matching11 .collect(); // [6, 8, 10]1213// other adaptors14(1..=3).rev(); // 3, 2, 115(1..=5).enumerate(); // (0,1), (1,2), ...16(1..=10).skip(2).take(3); // 3, 4, 517[1,2,3].iter().sum::<i32>(); // 618(0..5).any(|x| x == 3); // true19(0..5).all(|x| x < 10); // true
Lifetimes
A lifetime tells the compiler how long a reference is valid. You rarely write them — elision infers the common cases. Write them when a function returns a borrow tied to an input.
1// elision handles the common case for you2fn first_word(s: &str) -> &str { // implicit same lifetime3 s.split_whitespace().next().unwrap_or("")4}56// explicit: tie the output to an input7fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {8 if x.len() > y.len() { x } else { y }9}1011// structs holding references need a lifetime12struct Parser<'src> { input: &'src str }13impl<'src> Parser<'src> {14 fn rest(&self) -> &'src str { self.input }15}1617// 'static lives for the entire program — string literals have it18let s: &'static str = "I live forever";1920// in generics, T: 'a means T's refs outlive 'a21fn foo<T: 'a>(x: &'a T) {}
Popular Crates
Add with cargo add <name>. These are the
load-bearing dependencies in most real Rust codebases.
| Crate | What it's for |
|---|---|
serde | serialization |
tokio | async runtime |
anyhow | app errors |
thiserror | library errors |
clap | CLI parsing |
reqwest | HTTP client |
rand | randomness |
rayon | data parallelism |
itertools | iterator extras |
tracing | structured logging |
chrono | dates & times |
sqlx | async SQL |
1// serde — derive Serialize / Deserialize2#[derive(serde::Serialize, serde::Deserialize)]3struct User { name: String, age: u32 }45// anyhow — ergonomic boxed errors for apps6fn read_cfg() -> anyhow::Result<String> {7 Ok(std::fs::read_to_string("cfg.toml")8 .context("reading cfg.toml")?)9}1011// thiserror — typed enum errors for libraries12#[derive(Debug, thiserror::Error)]13enum AppError {14 #[error("io: {0}")]15 Io(#[from] std::io::Error),16}1718// tokio + reqwest — async HTTP19#[tokio::main]20async fn main() {21 let body = reqwest::get("https://a.io")22 .await?.text().await?;23}2425// rayon — parallel iterators26use rayon::prelude::*;27let sum: i32 = (0..1_000_000)28 .into_par_iter().map(|x| x * 2).sum();