Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees memory safety — without a garbage collector. It has been voted the most loved programming language on Stack Overflow for nine consecutive years. It powers everything from WebAssembly to embedded firmware to operating system kernels. This tutorial takes you from zero to writing real Rust programs, step by step, with no prior Rust experience required.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install Rust and write your first program |
| Syntax & variables | Understand how Rust code is structured |
| Ownership & borrowing | Understand Rust's core memory model |
| Types | Work with integers, floats, strings, booleans |
| Control flow | Use if/else, loop, while, and for |
| Structs & enums | Model data with custom types |
| Collections | Use Vec, HashMap, and HashSet |
| Error handling | Use Result and Option idiomatically |
| Traits | Write flexible, reusable abstractions |
| Closures & iterators | Write functional-style data pipelines |
| Modules | Organise code into modules and crates |
| Projects | Build 3 real programs |
Rust version used: Rust 1.80+ (stable)
Part 1 — Why Rust?
Rust was designed at Mozilla to solve systems programming problems: memory bugs, data races, and unpredictable performance. The compiler catches entire classes of bugs at compile time that other languages only find at runtime.
Rust use cases
| Domain | Examples |
|---|---|
| Systems programming | Operating systems, device drivers |
| WebAssembly | Blazingly fast browser code |
| CLI tools | ripgrep, fd, bat, exa |
| Web backends | Axum, Actix-web APIs |
| Embedded / IoT | No-std firmware, microcontrollers |
| Blockchain | Solana, Near Protocol |
| Game development | Bevy engine |
| Networking | Cloudflare services, Tokio async runtime |
Rust vs C++ vs Go vs Python
| Dimension | Rust | C++ | Go | Python |
|---|---|---|---|---|
| Speed | Native (no GC) | Native (no GC) | Very fast (GC) | Slow (interpreted) |
| Memory safety | Guaranteed at compile time | Manual (error-prone) | GC handles it | GC handles it |
| Concurrency | Fearless (data race-free) | Error-prone | Goroutines | GIL-limited |
| Learning curve | Steep | Very steep | Gentle | Very gentle |
| Ecosystem | Growing fast | Mature | Growing | Enormous |
| Best for | Safety-critical systems | Legacy systems | Cloud services | Data, scripting |
Part 2 — Setup
Install Rust
The official way to install Rust is through rustup, the Rust toolchain manager.
# Windows (run in PowerShell)
winget install Rustlang.Rustup
# or download from https://rustup.rs and run the installer
# macOS / Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
After installation, open a new terminal and verify:
rustc --version # rustc 1.80.0 (or newer)
cargo --version # cargo 1.80.0 (or newer)
What gets installed
| Tool | Purpose |
|---|---|
rustc |
The Rust compiler |
cargo |
Build system and package manager |
rustup |
Toolchain manager (update Rust) |
rust-std |
Standard library |
Recommended editor
VS Code with the rust-analyzer extension gives you the best experience: inline type hints, autocompletion, and real-time error checking.
Your first Rust program
cargo new hello-rust
cd hello-rust
This creates:
hello-rust/
├── Cargo.toml # project manifest (like package.json)
└── src/
└── main.rs # entry point
Open src/main.rs:
fn main() {
println!("Hello, Rust!");
}
Run it:
cargo run
# Hello, Rust!
println! is a macro (note the !). Macros are expanded at compile time and are more powerful than regular functions.
Part 3 — Variables and Types
Variables and mutability
In Rust, variables are immutable by default. This is intentional — it prevents accidental mutation.
fn main() {
let x = 5;
// x = 6; // ERROR: cannot assign twice to immutable variable
let mut y = 10; // mut makes it mutable
y = 20;
println!("y = {y}");
// Shadowing: redeclare with the same name
let z = "hello";
let z = z.len(); // z is now usize, not &str
println!("z = {z}");
// Constants: compile-time, always immutable, must have type
const MAX_POINTS: u32 = 100_000;
println!("max = {MAX_POINTS}");
}
Scalar types
| Type | Description | Example |
|---|---|---|
i8, i16, i32, i64, i128 |
Signed integers | let n: i32 = -42; |
u8, u16, u32, u64, u128 |
Unsigned integers | let n: u8 = 255; |
isize, usize |
Pointer-sized integer | let i: usize = 0; |
f32, f64 |
Floating point | let f: f64 = 3.14; |
bool |
Boolean | let b: bool = true; |
char |
Unicode scalar value (4 bytes) | let c: char = '😊'; |
Rust infers types in most cases. When it can't, you must annotate:
fn main() {
let x = 42; // inferred: i32
let y = 3.14; // inferred: f64
let z: u8 = 200; // explicit annotation
// Type conversion is always explicit (no implicit casting)
let a: i32 = 10;
let b: f64 = a as f64; // cast with `as`
// Integer arithmetic
println!("{}", 10 / 3); // 3 (integer division)
println!("{}", 10 % 3); // 1 (remainder)
println!("{}", 2_i32.pow(10)); // 1024
// Underscores for readability
let million = 1_000_000_u64;
println!("{million}");
}
Strings
Rust has two main string types:
| Type | Description | Stored on |
|---|---|---|
&str |
String slice — immutable reference to string data | Stack / static memory |
String |
Owned, growable string | Heap |
fn main() {
// &str — string literal (hardcoded in binary)
let greeting: &str = "Hello";
// String — owned, heap-allocated
let mut name = String::from("Alice");
name.push_str(", welcome!");
name.push('!');
// Format strings
let full = format!("{greeting}, {name}");
println!("{full}");
// Useful String methods
let s = String::from(" Hello, World! ");
println!("{}", s.trim()); // "Hello, World!"
println!("{}", s.to_lowercase()); // " hello, world! "
println!("{}", s.contains("World")); // true
println!("{}", s.replace("World", "Rust")); // " Hello, Rust! "
println!("{}", s.len()); // 17 (bytes)
// Split and collect
let csv = "a,b,c,d";
let parts: Vec<&str> = csv.split(',').collect();
println!("{:?}", parts); // ["a", "b", "c", "d"]
}
Part 4 — Ownership and Borrowing
Ownership is Rust's most distinctive feature. It's how Rust achieves memory safety without a garbage collector.
The three rules of ownership
- Each value in Rust has one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
Move semantics
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2 — s1 is no longer valid
// println!("{s1}"); // ERROR: value used after move
// Clone makes a deep copy — both are valid
let s3 = String::from("world");
let s4 = s3.clone();
println!("{s3} and {s4}"); // both valid
// Copy types (integers, floats, bool, char) are copied, not moved
let x = 5;
let y = x;
println!("{x} and {y}"); // both valid — i32 implements Copy
}
References and borrowing
Instead of moving ownership, you can borrow a reference:
fn main() {
let s = String::from("hello");
// Immutable reference: you can have many at once
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // both valid
// Mutable reference: only ONE at a time
let mut t = String::from("hello");
let mr = &mut t;
mr.push_str(", world");
println!("{mr}");
// You cannot mix mutable and immutable references in the same scope
// let r3 = &t; // ERROR if mr is still in use
}
// Function that borrows (doesn't take ownership)
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it's a reference — nothing is dropped
fn main_2() {
let s = String::from("hello");
let len = calculate_length(&s); // borrow with &
println!("{s} has {len} chars"); // s still valid!
}
The borrow checker rules summary
| Rule | What it prevents |
|---|---|
| One owner at a time | Double-free bugs |
| Immutable refs OR one mutable ref | Data races |
| References must not outlive the data | Dangling pointers |
Slices
A slice is a reference to a contiguous portion of a collection:
fn main() {
let s = String::from("hello world");
// String slice
let hello = &s[0..5]; // or &s[..5]
let world = &s[6..11]; // or &s[6..]
println!("{hello} {world}");
// Array slice
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // [2, 3]
println!("{:?}", slice);
}
Part 5 — Control Flow
if / else
fn main() {
let n = 42;
if n < 0 {
println!("negative");
} else if n == 0 {
println!("zero");
} else {
println!("positive");
}
// if is an expression — it returns a value
let description = if n % 2 == 0 { "even" } else { "odd" };
println!("{n} is {description}");
}
Loops
fn main() {
// loop: infinite, use `break` to exit (can return a value)
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // loop returns 20
}
};
println!("result = {result}");
// while
let mut n = 1;
while n < 100 {
n *= 2;
}
println!("n = {n}");
// for — the most common loop in Rust
for i in 0..5 {
print!("{i} "); // 0 1 2 3 4
}
println!();
for i in (0..5).rev() {
print!("{i} "); // 4 3 2 1 0
}
println!();
// Iterate over a collection
let fruits = ["apple", "banana", "cherry"];
for fruit in &fruits {
println!("{fruit}");
}
// With index
for (i, fruit) in fruits.iter().enumerate() {
println!("{i}: {fruit}");
}
}
Pattern matching with match
match in Rust is exhaustive — you must handle every case:
fn main() {
let n = 7;
match n {
1 => println!("one"),
2 | 3 => println!("two or three"), // multiple patterns
4..=6 => println!("four through six"), // range
x if x % 2 == 0 => println!("{x} is even"), // guard
_ => println!("something else"), // wildcard (catch-all)
}
// match is an expression
let grade = match n {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "F",
};
println!("grade: {grade}");
}
Part 6 — Functions
// Basic function — return type after ->
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = implicit return (expression)
}
// Multiple return values via tuple
fn min_max(nums: &[i32]) -> (i32, i32) {
let min = *nums.iter().min().unwrap();
let max = *nums.iter().max().unwrap();
(min, max)
}
// Destructuring the tuple
fn main() {
println!("{}", add(3, 4)); // 7
let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
let (min, max) = min_max(&numbers);
println!("min={min}, max={max}");
}
Part 7 — Structs
// Define a struct
#[derive(Debug)] // enables {:?} formatting
struct Point {
x: f64,
y: f64,
}
// Methods go in an impl block
impl Point {
// Associated function (like a static method) — no `self`
fn new(x: f64, y: f64) -> Self {
Self { x, y } // shorthand when field names match variable names
}
// Method — takes self by reference
fn distance_to_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
// Mutable method
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let mut p = Point::new(3.0, 4.0);
println!("{:?}", p); // Point { x: 3.0, y: 4.0 }
println!("distance: {}", p.distance_to_origin()); // 5.0
p.translate(1.0, 1.0);
println!("{:?}", p); // Point { x: 4.0, y: 5.0 }
}
Tuple structs and unit structs
// Tuple struct: fields accessed by position
struct Meters(f64);
struct Kilograms(f64);
fn main() {
let distance = Meters(1000.0);
let weight = Kilograms(75.0);
println!("{} meters", distance.0);
// distance + weight would be a type error — they're different types!
// Unit struct: no fields, used as markers
struct AlwaysEqual;
let _subject = AlwaysEqual;
}
Part 8 — Enums
Rust enums are much more powerful than enums in C or Java — each variant can hold data:
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle { base: f64, height: f64 }, // named fields
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle { base: 3.0, height: 8.0 },
];
for shape in &shapes {
println!("{shape:?} => area = {:.2}", shape.area());
}
}
Option — handling the absence of a value
Rust has no null. Instead, it uses Option<T>:
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
// Pattern match on Option
match divide(10.0, 2.0) {
Some(result) => println!("result: {result}"),
None => println!("cannot divide by zero"),
}
// if let — when you only care about one variant
if let Some(result) = divide(10.0, 0.0) {
println!("result: {result}");
} else {
println!("division failed");
}
// Useful Option methods
let val: Option<i32> = Some(42);
println!("{}", val.unwrap()); // 42 (panics if None!)
println!("{}", val.unwrap_or(0)); // 42, or 0 if None
println!("{}", val.unwrap_or_default());// 42, or i32::default() (0)
println!("{}", val.is_some()); // true
println!("{}", val.is_none()); // false
// map transforms the inner value
let doubled = val.map(|x| x * 2);
println!("{:?}", doubled); // Some(84)
}
Part 9 — Error Handling
Result — handling recoverable errors
use std::fs;
use std::num::ParseIntError;
// Functions that can fail return Result<T, E>
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse::<i32>() // parse returns Result
}
fn main() {
// Pattern match on Result
match parse_number("42") {
Ok(n) => println!("parsed: {n}"),
Err(e) => println!("error: {e}"),
}
// The ? operator — propagate errors up the call stack
// (only works in functions that return Result or Option)
fn read_username() -> Result<String, std::io::Error> {
let content = fs::read_to_string("username.txt")?; // ? = return Err if fail
Ok(content.trim().to_string())
}
// unwrap_or, unwrap_or_else for defaults
let n = parse_number("abc").unwrap_or(0);
println!("n = {n}");
let n2 = parse_number("abc").unwrap_or_else(|e| {
eprintln!("warning: {e}");
-1
});
println!("n2 = {n2}");
}
Custom error types
use std::fmt;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
InvalidInput(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {e}"),
AppError::ParseError(e) => write!(f, "parse error: {e}"),
AppError::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
}
}
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::IoError(e)
}
}
fn process(s: &str) -> Result<i32, AppError> {
if s.is_empty() {
return Err(AppError::InvalidInput("empty string".to_string()));
}
let n = s.trim().parse::<i32>().map_err(AppError::ParseError)?;
Ok(n * 2)
}
Part 10 — Collections
Vec — growable array
fn main() {
// Create
let mut v: Vec<i32> = Vec::new();
let mut v2 = vec![1, 2, 3]; // vec! macro
// Add / remove
v2.push(4);
v2.push(5);
let last = v2.pop(); // Some(5)
println!("{:?}, popped: {:?}", v2, last);
// Access
let third = &v2[2]; // panics if out of bounds
let maybe = v2.get(10); // returns Option<&i32>
println!("third: {third}, maybe: {maybe:?}");
// Iterate
for item in &v2 {
print!("{item} ");
}
println!();
// Common operations
v2.sort();
v2.dedup(); // remove consecutive duplicates (sort first!)
v2.retain(|&x| x > 1); // keep only elements where closure returns true
println!("{:?}", v2);
// Useful methods
println!("len: {}", v2.len());
println!("contains 3: {}", v2.contains(&3));
println!("sum: {}", v2.iter().sum::<i32>());
}
HashMap
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, u32> = HashMap::new();
// Insert
scores.insert("Alice".to_string(), 95);
scores.insert("Bob".to_string(), 87);
scores.insert("Charlie".to_string(), 92);
// Access — returns Option
if let Some(score) = scores.get("Alice") {
println!("Alice: {score}");
}
// Insert only if key doesn't exist
scores.entry("Dave".to_string()).or_insert(75);
// Update based on current value
let count = scores.entry("Alice".to_string()).or_insert(0);
*count += 5;
// Iterate (order not guaranteed)
for (name, score) in &scores {
println!("{name}: {score}");
}
// Useful methods
println!("has Bob: {}", scores.contains_key("Bob"));
scores.remove("Bob");
println!("len: {}", scores.len());
}
HashSet
use std::collections::HashSet;
fn main() {
let mut a: HashSet<i32> = [1, 2, 3, 4, 5].iter().cloned().collect();
let b: HashSet<i32> = [3, 4, 5, 6, 7].iter().cloned().collect();
a.insert(6);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));
// Set operations
let union: HashSet<_> = a.union(&b).collect();
let intersection: HashSet<_> = a.intersection(&b).collect();
let difference: HashSet<_> = a.difference(&b).collect();
println!("union: {:?}", union);
println!("intersection: {:?}", intersection);
println!("difference (a-b): {:?}", difference);
}
Part 11 — Traits
Traits are Rust's way of defining shared behaviour — similar to interfaces in other languages:
// Define a trait
trait Greet {
fn greeting(&self) -> String;
// Default implementation
fn greet(&self) {
println!("{}", self.greeting());
}
}
struct English;
struct Spanish;
impl Greet for English {
fn greeting(&self) -> String {
"Hello!".to_string()
}
}
impl Greet for Spanish {
fn greeting(&self) -> String {
"¡Hola!".to_string()
}
// greet() uses default implementation
}
// Trait bounds — accept any type that implements Greet
fn print_greeting(g: &impl Greet) {
g.greet();
}
// Generic version (equivalent)
fn print_greeting_generic<T: Greet>(g: &T) {
g.greet();
}
fn main() {
let en = English;
let es = Spanish;
print_greeting(&en); // Hello!
print_greeting(&es); // ¡Hola!
}
Commonly used standard traits
| Trait | What it enables | How to derive |
|---|---|---|
Debug |
{:?} formatting |
#[derive(Debug)] |
Display |
{} formatting |
Manual impl |
Clone |
.clone() deep copy |
#[derive(Clone)] |
Copy |
Implicit copy (like i32) |
#[derive(Copy, Clone)] |
PartialEq, Eq |
== comparison |
#[derive(PartialEq, Eq)] |
PartialOrd, Ord |
<, >, .sort() |
#[derive(PartialOrd, Ord)] |
Hash |
Use in HashMap keys |
#[derive(Hash)] |
Default |
.unwrap_or_default() |
#[derive(Default)] |
Iterator |
for loops, .map(), etc. |
Manual impl |
Part 12 — Closures and Iterators
Closures
Closures are anonymous functions that can capture variables from their environment:
fn main() {
// Closure syntax
let add = |a: i32, b: i32| a + b;
println!("{}", add(3, 4));
// Closures capture their environment
let multiplier = 3;
let triple = |x| x * multiplier; // captures multiplier
println!("{}", triple(5)); // 15
// Move closure — takes ownership of captured variables
let name = String::from("Rust");
let greet = move || println!("Hello, {name}!");
greet();
// name is moved into greet — can't use it here
}
Iterator adapters
Rust's iterator system is lazy and composable:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map, filter, collect
let even_squares: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("{:?}", even_squares); // [4, 16, 36, 64, 100]
// sum, product, count
let sum: i32 = numbers.iter().sum();
let product: i32 = (1..=5).product();
let count = numbers.iter().filter(|&&x| x > 5).count();
println!("sum={sum}, product={product}, count={count}");
// any, all
let has_even = numbers.iter().any(|&x| x % 2 == 0);
let all_positive = numbers.iter().all(|&x| x > 0);
println!("has_even={has_even}, all_positive={all_positive}");
// find, position
let first_even = numbers.iter().find(|&&x| x % 2 == 0);
println!("first even: {first_even:?}"); // Some(2)
// zip two iterators
let letters = ['a', 'b', 'c'];
let paired: Vec<_> = (1..=3).zip(letters.iter()).collect();
println!("{:?}", paired); // [(1, 'a'), (2, 'b'), (3, 'c')]
// flat_map (map + flatten)
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", chars); // ["hello", "world", "foo", "bar"]
// fold (reduce)
let factorial: u64 = (1..=10).fold(1, |acc, x| acc * x);
println!("10! = {factorial}");
}
Part 13 — Modules and Crates
Organising code with modules
// src/main.rs
mod math {
// Items are private by default — `pub` makes them public
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
pub mod advanced {
pub fn power(base: i32, exp: u32) -> i32 {
base.pow(exp)
}
}
}
use math::add; // bring into scope
use math::advanced::power;
fn main() {
println!("{}", add(3, 4)); // 7
println!("{}", math::subtract(10, 3)); // 7 (full path)
println!("{}", power(2, 10)); // 1024
}
Using external crates
Add dependencies in Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
Then run cargo build to download and compile.
// Using rand
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen_range(1..=100);
println!("random: {n}");
}
Essential crates
| Crate | Purpose |
|---|---|
serde + serde_json |
Serialisation / JSON |
tokio |
Async runtime |
reqwest |
HTTP client |
axum |
Web framework |
clap |
CLI argument parsing |
rand |
Random number generation |
chrono |
Date and time |
log + env_logger |
Logging |
anyhow |
Easy error handling |
thiserror |
Custom error types |
Part 14 — Three Projects
Project 1: CLI To-Do Manager
// src/main.rs
use std::env;
#[derive(Debug)]
struct Todo {
id: usize,
text: String,
done: bool,
}
struct TodoList {
items: Vec<Todo>,
next_id: usize,
}
impl TodoList {
fn new() -> Self {
Self { items: Vec::new(), next_id: 1 }
}
fn add(&mut self, text: &str) {
self.items.push(Todo {
id: self.next_id,
text: text.to_string(),
done: false,
});
self.next_id += 1;
println!("Added: {text}");
}
fn complete(&mut self, id: usize) {
match self.items.iter_mut().find(|t| t.id == id) {
Some(todo) => {
todo.done = true;
println!("Completed: {}", todo.text);
}
None => println!("No todo with id {id}"),
}
}
fn list(&self) {
if self.items.is_empty() {
println!("No todos.");
return;
}
for todo in &self.items {
let status = if todo.done { "✓" } else { "○" };
println!("[{status}] {} — {}", todo.id, todo.text);
}
}
fn remove(&mut self, id: usize) {
let before = self.items.len();
self.items.retain(|t| t.id != id);
if self.items.len() < before {
println!("Removed todo {id}");
} else {
println!("No todo with id {id}");
}
}
}
fn main() {
let mut list = TodoList::new();
let args: Vec<String> = env::args().collect();
match args.get(1).map(String::as_str) {
Some("add") => {
if let Some(text) = args.get(2) {
list.add(text);
} else {
eprintln!("Usage: todo add <text>");
}
}
Some("done") => {
if let Some(id) = args.get(2).and_then(|s| s.parse().ok()) {
list.complete(id);
}
}
Some("rm") => {
if let Some(id) = args.get(2).and_then(|s| s.parse().ok()) {
list.remove(id);
}
}
Some("ls") | None => list.list(),
Some(cmd) => eprintln!("Unknown command: {cmd}"),
}
}
Run it:
cargo run -- add "Learn Rust"
cargo run -- add "Build a project"
cargo run -- ls
cargo run -- done 1
cargo run -- rm 2
Project 2: Word Frequency Counter
// Cargo.toml: no extra dependencies
use std::collections::HashMap;
use std::env;
use std::fs;
fn count_words(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
// Normalise: lowercase, strip punctuation
let clean: String = word
.chars()
.filter(|c| c.is_alphabetic())
.collect::<String>()
.to_lowercase();
if !clean.is_empty() {
*counts.entry(clean).or_insert(0) += 1;
}
}
counts
}
fn top_n(counts: &HashMap<String, usize>, n: usize) -> Vec<(&String, &usize)> {
let mut sorted: Vec<_> = counts.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
sorted.into_iter().take(n).collect()
}
fn main() {
let args: Vec<String> = env::args().collect();
let path = args.get(1).map(String::as_str).unwrap_or("input.txt");
let n: usize = args.get(2)
.and_then(|s| s.parse().ok())
.unwrap_or(10);
let text = match fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
eprintln!("Error reading {path}: {e}");
std::process::exit(1);
}
};
let counts = count_words(&text);
let total_words: usize = counts.values().sum();
let unique_words = counts.len();
println!("Total words: {total_words}");
println!("Unique words: {unique_words}");
println!("\nTop {n} words:");
println!("{:<20} {:<10} {}", "Word", "Count", "Frequency");
println!("{}", "-".repeat(42));
for (word, count) in top_n(&counts, n) {
let freq = (*count as f64 / total_words as f64) * 100.0;
println!("{:<20} {:<10} {:.2}%", word, count, freq);
}
}
Run it:
echo "the quick brown fox jumps over the lazy dog the fox" > input.txt
cargo run -- input.txt 5
Project 3: Simple HTTP API (with Axum)
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
// src/main.rs
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{delete, get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Note {
id: u64,
title: String,
body: String,
}
#[derive(Deserialize)]
struct CreateNote {
title: String,
body: String,
}
type Db = Arc<Mutex<Vec<Note>>>;
async fn list_notes(State(db): State<Db>) -> Json<Vec<Note>> {
let notes = db.lock().unwrap();
Json(notes.clone())
}
async fn create_note(
State(db): State<Db>,
Json(payload): Json<CreateNote>,
) -> (StatusCode, Json<Note>) {
let mut notes = db.lock().unwrap();
let id = notes.len() as u64 + 1;
let note = Note {
id,
title: payload.title,
body: payload.body,
};
notes.push(note.clone());
(StatusCode::CREATED, Json(note))
}
async fn get_note(
State(db): State<Db>,
Path(id): Path<u64>,
) -> Result<Json<Note>, StatusCode> {
let notes = db.lock().unwrap();
notes.iter()
.find(|n| n.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn delete_note(
State(db): State<Db>,
Path(id): Path<u64>,
) -> StatusCode {
let mut notes = db.lock().unwrap();
let before = notes.len();
notes.retain(|n| n.id != id);
if notes.len() < before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/notes", get(list_notes).post(create_note))
.route("/notes/:id", get(get_note).delete(delete_note))
.with_state(db);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Listening on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Test it:
cargo run &
curl -X POST http://localhost:3000/notes \
-H "Content-Type: application/json" \
-d '{"title":"First note","body":"Hello Rust!"}'
curl http://localhost:3000/notes
curl http://localhost:3000/notes/1
curl -X DELETE http://localhost:3000/notes/1
Learning path
| Stage | Duration | What to learn |
|---|---|---|
| Beginner | Weeks 1–4 | Ownership, basic types, enums, pattern matching |
| Getting comfortable | Weeks 5–8 | Traits, generics, error handling, collections |
| Intermediate | Weeks 9–16 | Lifetimes, closures, iterators, modules, Cargo |
| Async | Weeks 17–20 | Tokio, async/await, futures |
| Systems / advanced | Weeks 21–28 | Unsafe Rust, macros, FFI, no-std |
| Specialise | Ongoing | Pick a domain (web/systems/embedded/WebAssembly) |
Best resource: The Rust Book — free, official, excellent. Also: Rustlings for exercises and Rust by Example.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Fighting the borrow checker | Trying to write non-Rust patterns in Rust | Learn ownership; restructure code rather than force-fitting |
Using unwrap() everywhere |
Panics at runtime in production | Use ? operator, unwrap_or, or proper error handling |
clone()ing to avoid borrow errors |
Performance cost, may indicate design issue | Use references, restructure, or clone only where necessary |
Using String when &str suffices |
Unnecessary heap allocation | Prefer &str for function parameters that just read |
| Not using iterators | Verbose, often less efficient | Replace for loops with .iter().map().filter().collect() |
| Ignoring compiler warnings | Warnings often indicate real bugs | Run cargo clippy and address all warnings |
| Skipping lifetimes until forced | Confusing when you first encounter them | Read the lifetime chapter in the Rust Book early |
| Not reading error messages fully | Rust error messages are excellent — they tell you what to do | Read the full error including the "help:" suggestions |
Rust vs related languages and tools
| Term | Relationship to Rust |
|---|---|
| Cargo | Rust's official build system and package manager |
| crates.io | The official Rust package registry (like npm/PyPI) |
| Tokio | The most popular async runtime for Rust |
| rustup | Manages Rust compiler versions and toolchains |
| clippy | Rust's official linter (cargo clippy) |
| rustfmt | Rust's official formatter (cargo fmt) |
| unsafe Rust | Opt-in subset that allows raw pointers and unsafe operations |
| WebAssembly (WASM) | Rust compiles to WASM — run Rust in the browser |
| no_std | A Rust subset for embedded systems with no OS |
| C FFI | Rust can call C code and be called from C |
FAQ
Do I need to know C or C++ to learn Rust? No. While Rust is often compared to C++, you don't need to know either. Having any programming experience (Python, JavaScript, Go, Java) is enough. C/C++ experience can help you understand why ownership matters, but it's not required.
How long does it take to become productive in Rust? Expect 2–4 weeks before the borrow checker stops fighting you, and 2–3 months before you feel fluent. Rust has a steeper learning curve than most languages, but the skills you learn transfer to a deep understanding of memory and concurrency that benefits your work in all languages.
What's the deal with lifetimes? Lifetimes are annotations that tell the compiler how long references are valid. Most of the time, the compiler infers them automatically (lifetime elision). You only need to write them explicitly in function signatures and structs that hold references — which is less common than beginners expect.
Should I use async Rust right away? No. Learn sync Rust first. Async Rust (Tokio, async/await) adds significant complexity. Build a few sync programs, get comfortable with ownership and traits, then tackle async.
Is Rust worth learning for web development? Yes, but it's not the easiest path. Axum and Actix-web are excellent frameworks, and Rust excels at high-performance APIs. However, Node.js, Go, or Python/Django will get you to a working web app faster. Rust is worth it if you need maximum performance or are building infrastructure.
Where is Rust used in production? Mozilla (the Servo browser engine), Cloudflare (networking services), Discord (voice/video infrastructure), Amazon (AWS Firecracker hypervisor, Bottlerocket OS), Microsoft (Windows kernel components), Linux kernel (Rust for Linux), Google (Android OS components), Meta (Folly library, Diem blockchain).