TL;DR
Rust gives you C/C++ performance with memory safety guaranteed at compile time — no garbage collector, no null pointer exceptions, no data races. Learn ownership (each value has one owner), borrowing (&T for read, &mut T for write), and lifetimes (how long references are valid). Use Result<T,E> for recoverable errors and Option<T> for nullable values. For async web services use Axum or Actix-web on top of Tokio runtime. Cargo is the best-in-class package manager: cargo new, cargo build, cargo test, cargo doc.
Key Takeaways
- Rust eliminates memory safety issues at compile time through the ownership system — no garbage collector, performance equivalent to C/C++.
- Every value has exactly one owner; borrowing rules: either multiple &T or exactly one &mut T at a time — never both simultaneously.
- Use Result<T,E> for recoverable errors and Option<T> for nullable values; the ? operator propagates errors automatically — avoid unwrap() in production.
- Traits are Rust's polymorphism mechanism; generic bounds (T: Trait) give zero-cost static dispatch while dyn Trait enables runtime dynamic dispatch.
- Tokio is the dominant async runtime; Axum and Actix-web are the top choices for building high-performance web APIs.
- Cargo is a best-in-class package manager and build tool: cargo new, cargo build, cargo test, cargo clippy, and cargo doc cover the entire development workflow.
1. What Is Rust and Why Do Developers Love It?
Rust is a systems programming language started by Mozilla in 2010 and officially released in 2015. Its goal is to deliver C/C++-level performance while guaranteeing memory safety at compile time through a unique ownership system — no garbage collector required. Rust has topped Stack Overflow's "Most Loved Language" survey for nine consecutive years (2016–2024) and has been adopted by the Linux kernel, Windows, Android, Firefox, Cloudflare, AWS, Meta, Google, and many other major projects.
The core problem Rust solves: roughly 70% of security vulnerabilities in C/C++ codebases stem from memory safety issues — dangling pointers, buffer overflows, data races. Rust eliminates these at compile time rather than relying on runtime garbage collection or manual memory management.
// Your first Rust program
fn main() {
// Variables are immutable by default
let name = "Rust";
let mut counter = 0; // mut makes it mutable
println!("Hello from {}!", name);
// Loops, conditions, and expressions
for i in 1..=5 {
counter += i;
}
println!("Sum 1..5 = {}", counter); // 15
// Everything is an expression
let result = if counter > 10 {
"greater than 10"
} else {
"10 or less"
};
println!("Result: {}", result);
}Rust vs C++ vs Go — Language Comparison
=========================================
Feature Rust C++ Go
------- ---- --- --
Memory Safety Compile-time Manual GC (runtime)
Garbage Collector No No Yes
Null Safety Option<T> Raw pointers nil (runtime)
Concurrency Safety Compile-time Manual Goroutines+GC
Performance C-equivalent C-equivalent ~20-50% slower
Compile Speed Slower Medium Very fast
Runtime Zero-size Zero-size Small runtime
Error Handling Result<T,E> Exceptions/codes errors (multiple return)
Generics Yes (monomorphic) Yes (templates) Yes (1.18+)
Async/Await Yes (Tokio) Yes (C++20) Goroutines
Package Manager Cargo vcpkg/Conan/... Go modules
Learning Curve Steep Very steep Gentle
Best For Systems, WebAsm Systems, games Cloud, CLIs, APIs
Choose Rust when:
- Memory safety is critical (security software, OS kernels)
- Zero-cost abstractions with no GC pauses (game engines, real-time)
- WebAssembly targets
- Embedded systems or resource-constrained environments
Choose C++ when:
- Existing large C++ codebase
- Graphics/game engines (Unreal, etc.)
- Maximum ecosystem maturity
Choose Go when:
- Cloud-native microservices and CLIs
- Team prefers simplicity over raw performance
- Fast build times are a priority2. Installing Rust and Creating Your First Project
Rust is installed through rustup, the toolchain manager, which handles multiple Rust versions (stable, beta, nightly) and cross-compilation targets. After installation, the rustc compiler, Cargo package manager, and standard library docs are all ready to use.
Installation and Toolchain Management
# Install rustup (macOS/Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows: download rustup-init.exe from https://rustup.rs
# Verify installation
rustc --version # rustc 1.76.0 (07dca489a 2024-02-04)
cargo --version # cargo 1.76.0
# Update Rust
rustup update
# Install nightly for experimental features
rustup install nightly
rustup default nightly
# Add a compilation target (cross-compile)
rustup target add wasm32-unknown-unknown # WebAssembly
rustup target add x86_64-pc-windows-gnu # Windows from Linux
# Install useful tools
cargo install cargo-watch # auto-rebuild on change
cargo install cargo-expand # expand macros
cargo install cargo-audit # security vulnerability audit
cargo install cargo-flamegraph # profiling
rustup component add clippy # linter
rustup component add rustfmt # formatterCargo: The Package Manager and Build System
# Create a new binary project
cargo new my-project
cd my-project
# Create a new library project
cargo new my-lib --lib
# Project structure
my-project/
Cargo.toml # project manifest (like package.json)
Cargo.lock # exact dependency versions (commit to VCS for bins)
src/
main.rs # entry point for binary
tests/ # integration tests
benches/ # benchmarks
examples/ # example programs
# Build commands
cargo build # debug build (fast compile, slow runtime)
cargo build --release # optimized build (slow compile, fast runtime)
cargo run # build + run
cargo run --release # optimized run
cargo test # run all tests
cargo test my_function # run tests matching a name
cargo doc --open # build and open documentation
cargo clippy # lint with Clippy
cargo fmt # format code
cargo check # fast type-check without producing binary
cargo clean # remove build artifacts
# Cargo.toml example
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
reqwest = { version = "0.11", features = ["json"] }
[dev-dependencies]
mockall = "0.11"
[profile.release]
opt-level = 3
lto = true # link-time optimization3. Ownership, Borrowing, and Lifetimes
The ownership system is Rust's most unique feature and the key to understanding the language. It enables Rust to guarantee memory safety without a garbage collector, and is the primary challenge for beginners — the borrow checker rejects unsafe code at compile time.
Ownership Rules and Move Semantics
fn main() {
// Rule 1: each value has one owner
let s1 = String::from("hello"); // s1 owns the String
// Rule 2: when owner goes out of scope, value is dropped
{
let s2 = String::from("world");
println!("{}", s2); // s2 valid here
} // s2 dropped here, memory freed automatically
// Move semantics: ownership transfer
let s3 = s1; // s1 is MOVED to s3
// println!("{}", s1); // ERROR: s1 is no longer valid!
println!("{}", s3); // OK
// Clone: explicit deep copy
let s4 = s3.clone();
println!("{} and {}", s3, s4); // both valid
// Copy types (stack-allocated primitives): implicit copy
let x = 5;
let y = x; // x is COPIED, not moved
println!("{} and {}", x, y); // both valid
// Types that implement Copy: i32, f64, bool, char, tuples of Copy types
}
// Ownership and functions
fn takes_ownership(s: String) { // s moves into this function
println!("{}", s);
} // s is dropped here
fn gives_ownership() -> String { // returns ownership to caller
String::from("mine")
}Borrowing and References
fn main() {
let s = String::from("hello");
// Immutable borrow: &T
let len = calculate_length(&s); // pass reference, not ownership
println!("Length of s is: {}", len); // s still valid here
// Mutable borrow: &mut T
let mut s2 = String::from("hello");
change(&mut s2);
println!("{}", s2); // "hello, world"
// Borrow rules demonstration
let mut s3 = String::from("test");
let r1 = &s3; // immutable borrow 1 - OK
let r2 = &s3; // immutable borrow 2 - OK (multiple &T allowed)
println!("{} {}", r1, r2); // last use of r1, r2
// r1 and r2 are no longer used after this point (NLL: Non-Lexical Lifetimes)
let r3 = &mut s3; // mutable borrow - OK (r1, r2 no longer active)
r3.push_str(" done");
// println!("{} {}", r1, r3); // ERROR: can't mix &T and &mut T
}
fn calculate_length(s: &String) -> usize {
s.len() // can read, cannot modify
}
fn change(s: &mut String) {
s.push_str(", world"); // can modify through &mut
}Lifetime Annotations
// Lifetime annotation: explicit when compiler can't infer
// 'a is a lifetime parameter ('a is conventional, any letter works)
// Without annotation this would fail — compiler doesn't know
// if the return value lives as long as x or y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
// 'a means: return value lives at least as long as the
// shorter of x and y's lifetimes
}
// Struct with a reference field — requires lifetime annotation
struct Excerpt<'a> {
text: &'a str, // text must live as long as this struct
}
impl<'a> Excerpt<'a> {
fn announce(&self, msg: &str) -> &str {
println!("Attention: {}", msg);
self.text // lifetime elision: return has same lifetime as &self
}
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result); // OK: result used within s2's scope
}
// println!("{}", result); // ERROR: s2 dropped, result might dangle
}4. Structs, Enums, and Pattern Matching
Rust's enums are far more powerful than enums in most languages — each variant can carry different types and amounts of data, making them algebraic data types (ADTs). Combined with exhaustive pattern matching, they enable type-safe and highly expressive code.
Structs and Methods
// Named struct
#[derive(Debug, Clone)] // auto-implement Debug and Clone traits
struct User {
username: String,
email: String,
active: bool,
login_count: u32,
}
// Tuple struct (named tuple)
struct Point(f64, f64, f64);
struct Color(u8, u8, u8);
// Unit struct (no fields, useful for traits)
struct AlwaysEqual;
// Implementing methods
impl User {
// Associated function (no self): constructor
fn new(username: String, email: String) -> Self {
User {
username,
email,
active: true,
login_count: 0,
}
}
// Method with immutable borrow
fn is_active(&self) -> bool {
self.active
}
// Method with mutable borrow
fn deactivate(&mut self) {
self.active = false;
}
// Method that consumes self
fn into_email(self) -> String {
self.email
}
}
fn main() {
let mut user = User::new(
String::from("alice"),
String::from("alice@example.com"),
);
println!("{:?}", user); // Debug output
user.deactivate();
// Struct update syntax
let user2 = User {
email: String::from("bob@example.com"),
..user // remaining fields from user (moved)
};
}Enums and Pattern Matching
// Enum with data in variants (ADT)
#[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,
}
}
}
// Message enum — common Rust pattern
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
// Pattern matching — must be exhaustive
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: rgb({},{},{})", r, g, b),
}
}
// if let — match a single pattern
fn check_coin(coin: Option<u32>) {
if let Some(value) = coin {
println!("Got {} cents", value);
} else {
println!("No coin");
}
}
// while let — loop until pattern fails
fn drain_stack(stack: &mut Vec<i32>) {
while let Some(top) = stack.pop() {
println!("{}", top);
}
}5. Error Handling with Result and Option
Rust has no exceptions. Error handling is done through two types: Option<T> for values that might not exist, and Result<T, E> for operations that might fail. This explicit error handling forces developers to address error paths at the point of writing, which is a major reason Rust programs are highly reliable.
use std::fs;
use std::num::ParseIntError;
// Option<T> — value might be absent
fn find_first_even(numbers: &[i32]) -> Option<i32> {
numbers.iter().find(|&&n| n % 2 == 0).copied()
}
// Result<T, E> — operation might fail
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse::<i32>()
}
// ? operator — early return on error
fn read_and_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // returns Err if file missing
let number = content.trim().parse::<i32>()?; // returns Err if not a number
Ok(number * 2)
}
// Handling Results with combinators
fn demonstrate_combinators() {
// map: transform Ok value
let doubled = parse_number("21").map(|n| n * 2);
println!("{:?}", doubled); // Ok(42)
// and_then: chain fallible operations
let result = parse_number("10")
.and_then(|n| if n > 0 { Ok(n) } else { Err("0".parse::<i32>().unwrap_err()) });
// unwrap_or: provide default value
let val = parse_number("abc").unwrap_or(0);
println!("{}", val); // 0
// unwrap_or_else: compute default lazily
let val2 = parse_number("abc").unwrap_or_else(|e| {
eprintln!("Parse error: {}", e);
-1
});
// Option combinators
let name: Option<&str> = Some(" alice ");
let trimmed = name.map(|s| s.trim());
let upper = name.map(|s| s.trim()).map(|s| s.to_uppercase());
// ok_or: convert Option to Result
let result: Result<&str, &str> = name.ok_or("no name provided");
}Custom Error Types and anyhow
use std::fmt;
// Custom error type — best practice for library code
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Custom(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::Custom(msg) => write!(f, "Error: {}", msg),
}
}
}
// impl From<> enables automatic ? conversion
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
fn process() -> Result<(), AppError> {
let content = std::fs::read_to_string("data.txt")?; // auto-converts io::Error
let num: i32 = content.trim().parse()?; // auto-converts ParseIntError
if num < 0 {
return Err(AppError::Custom("number must be positive".into()));
}
Ok(())
}
// --- OR use anyhow for application code ---
// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, anyhow};
fn process_with_anyhow(path: &str) -> Result<i32> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path))?;
let num: i32 = content.trim().parse()
.context("File does not contain a valid integer")?;
if num < 0 {
bail!("number {} must be non-negative", num);
}
Ok(num)
}6. Traits and Generics
Traits define shared behavior, similar to interfaces in other languages but with default implementations and operator overloading. Generics combined with trait bounds enable zero-cost abstractions: the compiler generates specialized code for each concrete type (monomorphization), with no runtime overhead.
// Define a trait
trait Summary {
fn summarize_author(&self) -> String; // required method
// Default implementation
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
#[derive(Debug)]
struct Article { title: String, author: String, content: String }
impl Summary for Article {
fn summarize_author(&self) -> String { self.author.clone() }
// Override default implementation
fn summarize(&self) -> String {
format!("{}, by {} — {}", self.title, self.author, &self.content[..50])
}
}
// Generics with trait bounds — static dispatch (zero cost)
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple bounds with + syntax
fn notify_debug<T: Summary + std::fmt::Debug>(item: &T) {
println!("{:?}", item);
println!("{}", item.summarize());
}
// Where clause for complex bounds
fn complex_function<T, U>(t: &T, u: &U) -> String
where
T: std::fmt::Display + Clone,
U: Summary + std::fmt::Debug,
{
format!("{} {}", t, u.summarize())
}
// impl Trait syntax (sugar for generics in simple cases)
fn notify_impl(item: &impl Summary) {
println!("{}", item.summarize());
}
// Return impl Trait — return type is opaque
fn make_summarizable() -> impl Summary {
Article {
title: String::from("Test"),
author: String::from("Alice"),
content: String::from("Content here"),
}
}Trait Objects — Dynamic Dispatch
// dyn Trait — runtime polymorphism via vtable (like virtual in C++)
struct Tweet { username: String, content: String }
impl Summary for Tweet {
fn summarize_author(&self) -> String { format!("@{}", self.username) }
}
// Vec of different types sharing a trait
fn print_all(items: &[Box<dyn Summary>]) {
for item in items {
println!("{}", item.summarize());
}
}
fn main() {
let article = Box::new(Article {
title: "Rust Rules".into(),
author: "Alice".into(),
content: "Lorem ipsum dolor sit amet consectetur adipiscing elit...".into(),
});
let tweet = Box::new(Tweet {
username: "bob".into(),
content: "Rust is amazing!".into(),
});
let items: Vec<Box<dyn Summary>> = vec![article, tweet];
print_all(&items);
}
// Important standard library traits to implement:
// Clone — explicit deep copy: #[derive(Clone)]
// Copy — implicit bitwise copy (stack types): #[derive(Copy, Clone)]
// Debug — {:?} formatting: #[derive(Debug)]
// Display — {} formatting: impl fmt::Display
// Iterator — .iter(), for loops: impl Iterator
// From/Into — type conversions: impl From<T>
// PartialEq — == operator: #[derive(PartialEq)]
// Hash — use as HashMap key: #[derive(Hash, Eq, PartialEq)]7. Closures and Iterators
Rust's iterators are lazy: calling adaptors like .map() and .filter() does not execute immediately — only when a consuming method (.collect(), .sum(), .for_each()) is called. This design makes chained operations as efficient as hand-written loops — the compiler can fully optimize away the iterator abstraction.
// Closures — anonymous functions that capture their environment
fn main() {
// Basic closure syntax
let add = |x, y| x + y; // type inferred
let square = |x: i32| -> i32 { x * x };
println!("{}", add(3, 4)); // 7
// Capture by reference (Fn)
let threshold = 10;
let is_big = |n: &i32| n > &threshold; // captures threshold by ref
// Capture by value (move)
let text = String::from("hello");
let contains_hello = move |s: &str| s.contains(&text); // moves text
// text is moved into closure, cannot use it here
// Three closure traits:
// Fn — borrows immutably, can be called multiple times
// FnMut — borrows mutably, can be called multiple times
// FnOnce — consumes captured values, called only once
let mut count = 0;
let mut increment = || { count += 1; count }; // FnMut
println!("{}", increment()); // 1
println!("{}", increment()); // 2
}// Iterator methods — zero-cost abstractions
fn iterator_examples() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: transform each element
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
// filter_map: filter + map in one step
let parsed: Vec<i32> = vec!["1", "two", "3", "four"]
.iter()
.filter_map(|s| s.parse().ok())
.collect();
// fold/reduce: accumulate a value
let sum = numbers.iter().fold(0, |acc, &n| acc + n); // 55
let sum2: i32 = numbers.iter().sum(); // shorthand for sum
// chain: concatenate iterators
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
// zip: pair two iterators
let pairs: Vec<(i32, i32)> = a.iter().zip(b.iter()).map(|(&x, &y)| (x, y)).collect();
// enumerate: add index
for (i, val) in numbers.iter().enumerate() {
println!("Index {}: {}", i, val);
}
// flat_map: map then flatten
let words = vec!["hello world", "foo bar"];
let all_words: Vec<&str> = words.iter().flat_map(|s| s.split_whitespace()).collect();
// take/skip: limit or offset
let first_three: Vec<i32> = numbers.iter().take(3).copied().collect();
let skip_first: Vec<i32> = numbers.iter().skip(7).copied().collect();
// any/all: boolean queries
let has_even = numbers.iter().any(|&n| n % 2 == 0); // true
let all_positive = numbers.iter().all(|&n| n > 0); // true
// max, min, count
let max = numbers.iter().max(); // Some(10)
let count = numbers.iter().count(); // 10
}8. Async Rust: Tokio Runtime and async/await
Rust's async/await is language-level syntax sugar — the compiler transforms async functions into state machines (implementing the Future trait). Unlike JavaScript's async/await, Rust Futures are lazy: creating a Future does not execute anything; it must be polled by a runtime (like Tokio) to make progress. Tokio is the dominant async runtime with a multi-threaded work-stealing scheduler.
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"use tokio::time::{sleep, Duration};
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
// #[tokio::main] initializes the Tokio runtime
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Basic async/await
let result = fetch_data("https://api.example.com/data").await?;
println!("{:?}", result);
// Concurrent tasks with tokio::spawn
let handle1 = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
println!("Task 1 done");
42
});
let handle2 = tokio::spawn(async {
sleep(Duration::from_millis(50)).await;
println!("Task 2 done");
24
});
let (r1, r2) = tokio::join!(handle1, handle2); // await both
println!("Results: {} {}", r1?, r2?);
// tokio::select! — race multiple futures
tokio::select! {
_ = sleep(Duration::from_millis(1000)) => println!("Timed out"),
result = fetch_data("https://api.example.com") => {
println!("Got result: {:?}", result);
}
}
// Shared state with Arc<Mutex<T>>
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(tokio::spawn(async move {
let mut lock = counter.lock().await;
*lock += 1;
}));
}
for h in handles { h.await?; }
println!("Final count: {}", counter.lock().await);
// Message passing with mpsc channels
let (tx, mut rx) = mpsc::channel::<String>(32);
tokio::spawn(async move {
tx.send("hello".to_string()).await.unwrap();
tx.send("world".to_string()).await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Received: {}", msg);
}
Ok(())
}
async fn fetch_data(url: &str) -> anyhow::Result<serde_json::Value> {
let response = reqwest::get(url).await?;
let json = response.json::<serde_json::Value>().await?;
Ok(json)
}9. Common Rust Data Structures: Vec, HashMap, and BTreeMap
The Rust standard library provides a carefully designed set of data structures. Vec<T> is the most commonly used dynamic array; HashMap<K, V> provides O(1) average lookup using SipHash (protection against hash flooding attacks); BTreeMap<K, V> maintains sorted key order with O(log n) lookup, suitable for scenarios requiring ordered iteration.
use std::collections::{HashMap, BTreeMap, HashSet, BTreeSet, VecDeque, BinaryHeap};
// Vec<T> — dynamic array (most common collection)
fn vec_examples() {
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3, 4, 5]; // macro shorthand
v.push(1); // O(1) amortized
v.pop(); // O(1)
v.insert(0, 99); // O(n)
v.remove(0); // O(n)
v.retain(|&x| x > 0); // keep elements matching predicate
v.sort(); // in-place sort O(n log n)
v.sort_by(|a, b| b.cmp(a)); // reverse sort
v.dedup(); // remove consecutive duplicates (sort first)
// Slices — view into a Vec
let slice: &[i32] = &v2[1..3]; // [2, 3]
let first = v2.first(); // Option<&i32>
let last = v2.last(); // Option<&i32>
let len = v2.len();
let is_empty = v2.is_empty();
// Pre-allocate capacity for performance
let mut v3: Vec<i32> = Vec::with_capacity(1000);
for i in 0..1000 { v3.push(i); } // no reallocation
}// HashMap<K, V> — O(1) average lookup
fn hashmap_examples() {
let mut scores: HashMap<String, i32> = HashMap::new();
// Insert
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
// entry API — insert if not present
scores.entry("Alice".to_string()).or_insert(0);
scores.entry("Carol".to_string()).or_insert(0); // inserts 0
// entry with modification
let count = scores.entry("Alice".to_string()).or_insert(0);
*count += 10; // now 110
// Lookup
let alice = scores.get("Alice"); // Option<&i32>
let bob = scores["Bob"]; // panics if missing!
let dave = scores.get("Dave").copied().unwrap_or(0);
// Iterate
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// Collect from iterator
let map: HashMap<&str, i32> = vec![("a", 1), ("b", 2)]
.into_iter()
.collect();
// Remove
scores.remove("Bob");
let contains = scores.contains_key("Alice"); // true
}
// BTreeMap<K, V> — sorted, O(log n) lookup
fn btreemap_examples() {
let mut map: BTreeMap<&str, i32> = BTreeMap::new();
map.insert("zebra", 1);
map.insert("apple", 2);
map.insert("mango", 3);
// Iterates in sorted key order: apple, mango, zebra
for (k, v) in &map { println!("{}: {}", k, v); }
// Range queries — unique to BTreeMap
use std::ops::Bound::Included;
for (k, v) in map.range("a"..="m") {
println!("{}: {}", k, v); // apple, mango
}
}// Other important collections
fn other_collections() {
// HashSet<T> — unordered unique values, O(1) lookup
let mut set: HashSet<i32> = HashSet::new();
set.insert(1); set.insert(2); set.insert(1); // dedup: {1, 2}
let contains = set.contains(&1); // true
let union: HashSet<_> = set.union(&HashSet::from([2, 3])).collect();
let intersection: HashSet<_> = set.intersection(&HashSet::from([1, 3])).collect();
// BTreeSet<T> — sorted unique values
let bset: BTreeSet<i32> = vec![3, 1, 4, 1, 5].into_iter().collect();
// Iterates in sorted order: 1, 3, 4, 5
// VecDeque<T> — double-ended queue, O(1) push/pop on both ends
let mut deque: VecDeque<i32> = VecDeque::new();
deque.push_back(1);
deque.push_front(0);
let front = deque.pop_front(); // Some(0)
let back = deque.pop_back(); // Some(1)
// BinaryHeap<T> — max-heap, O(log n) insert/pop
let mut heap: BinaryHeap<i32> = BinaryHeap::new();
heap.push(3); heap.push(1); heap.push(4); heap.push(2);
let max = heap.pop(); // Some(4)
}10. Rust for the Web: Axum and Actix-web
Rust's web frameworks have matured significantly. Axum (maintained by the Tokio team) and Actix-web are the two dominant choices. Both rank near the top in TechEmpower framework benchmarks, far outperforming most frameworks in other languages.
Axum: Modern Web Framework Built on Tower
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "uuid"] }
uuid = { version = "1", features = ["v4", "serde"] }use axum::{
extract::{Path, Query, State, Json},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post, put, delete},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Debug, Clone, Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Debug, Clone, Deserialize)]
struct PaginationQuery {
page: Option<u32>,
per_page: Option<u32>,
}
// Shared application state
type AppState = Arc<RwLock<Vec<User>>>;
#[tokio::main]
async fn main() {
let state: AppState = Arc::new(RwLock::new(vec![
User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
]));
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
.route("/health", get(|| async { "OK" }))
.with_state(state)
.layer(
tower_http::cors::CorsLayer::permissive()
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Listening on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
// GET /users?page=1&per_page=10
async fn list_users(
State(state): State<AppState>,
Query(params): Query<PaginationQuery>,
) -> Json<Vec<User>> {
let users = state.read().await;
let page = params.page.unwrap_or(1) as usize;
let per_page = params.per_page.unwrap_or(10) as usize;
let start = (page - 1) * per_page;
let paginated: Vec<User> = users.iter().skip(start).take(per_page).cloned().collect();
Json(paginated)
}
// GET /users/:id
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> Result<Json<User>, StatusCode> {
let users = state.read().await;
users.iter()
.find(|u| u.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
// POST /users
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
let mut users = state.write().await;
let id = users.len() as u32 + 1;
let user = User { id, name: payload.name, email: payload.email };
users.push(user.clone());
(StatusCode::CREATED, Json(user))
}
// PUT /users/:id
async fn update_user(
State(state): State<AppState>,
Path(id): Path<u32>,
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, StatusCode> {
let mut users = state.write().await;
if let Some(user) = users.iter_mut().find(|u| u.id == id) {
user.name = payload.name;
user.email = payload.email;
Ok(Json(user.clone()))
} else {
Err(StatusCode::NOT_FOUND)
}
}
// DELETE /users/:id
async fn delete_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> StatusCode {
let mut users = state.write().await;
let len_before = users.len();
users.retain(|u| u.id != id);
if users.len() < len_before { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND }
}Actix-web: High-Performance Actor-Model Web Framework
# Cargo.toml
[dependencies]
actix-web = "4"
actix-cors = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"use actix_web::{
web, App, HttpServer, HttpResponse, Responder,
middleware::Logger,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
struct Product {
id: u32,
name: String,
price: f64,
}
#[derive(Deserialize)]
struct CreateProduct {
name: String,
price: f64,
}
async fn get_products() -> impl Responder {
let products = vec![
Product { id: 1, name: "Widget".into(), price: 9.99 },
];
HttpResponse::Ok().json(products)
}
async fn create_product(body: web::Json<CreateProduct>) -> impl Responder {
let product = Product {
id: 1,
name: body.name.clone(),
price: body.price,
};
HttpResponse::Created().json(product)
}
async fn get_product_by_id(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().json(Product {
id,
name: format!("Product {}", id),
price: 19.99,
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.wrap(actix_cors::Cors::permissive())
.service(
web::scope("/api")
.route("/products", web::get().to(get_products))
.route("/products", web::post().to(create_product))
.route("/products/{id}", web::get().to(get_product_by_id))
)
})
.bind("0.0.0.0:8080")?
.run()
.await
}11. Testing in Rust: Unit Tests, Integration Tests, and Benchmarks
Rust has a built-in test framework requiring no additional dependencies. Unit tests live in a #[cfg(test)] module in the same file as the code under test and can access private functions. Integration tests live in the tests/ directory and can only access the public API. The cargo test command automatically discovers and runs all tests.
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 { Err("Division by zero".to_string()) }
else { Ok(a / b) }
}
// Unit tests — same file, #[cfg(test)] module
#[cfg(test)]
mod tests {
use super::*; // import everything from parent module
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
assert_ne!(add(1, 1), 3);
}
#[test]
fn test_divide_success() {
let result = divide(10.0, 2.0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 5.0);
}
#[test]
fn test_divide_by_zero() {
let result = divide(10.0, 0.0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Division by zero");
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic() {
let v = vec![1, 2, 3];
let _ = v[99]; // intentional panic
}
#[test]
#[ignore] // run with: cargo test -- --ignored
fn expensive_test() {
// slow test skipped by default
}
}
// Integration test: tests/integration_test.rs
// (separate file, can only use public API)
// use my_crate::{add, divide};
// #[test]
// fn test_public_api() {
// assert_eq!(add(10, 20), 30);
// }
// Run specific tests
// cargo test — run all tests
// cargo test test_add — run tests matching "test_add"
// cargo test -- --nocapture — show println! output
// cargo test -- --test-threads=1 — run single-threadedFrequently Asked Questions
What is Rust's ownership system?
Rust's ownership system is the core mechanism for memory safety without a garbage collector. Three rules: 1) Each value has exactly one owner; 2) When the owner goes out of scope, the value is automatically dropped (freed); 3) At any given time a value can have either one mutable reference (&mut T) OR any number of immutable references (&T), but not both simultaneously. These rules are enforced at compile time, eliminating dangling pointers, double frees, and data races.
What is the difference between borrowing and references in Rust?
A reference (&T) is the syntactic form; borrowing is the semantic concept. An immutable borrow &T allows reading but not modification, and multiple immutable borrows can coexist. A mutable borrow &mut T allows reading and writing, but only one mutable borrow of the same data can exist in a given scope, and it cannot coexist with immutable borrows. The borrow checker validates at compile time that all references remain valid for their entire lifetime, preventing dangling references.
What are lifetimes and when do I need to annotate them?
Lifetimes describe the scope for which a reference remains valid, preventing dangling references. The Rust compiler uses lifetime elision rules to infer lifetimes in most cases automatically. Explicit annotation is required when: a function returns a reference (the return lifetime must be tied to a parameter lifetime), a struct holds reference fields, or implementing methods with references in impl blocks. Syntax: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str — meaning the return value lives no longer than the shorter of x and y.
What is the difference between Result and Option in Rust?
Option<T> represents a value that may or may not exist — Some(T) or None — used for nullable values like hash map lookups or optional configuration. Result<T, E> represents an operation that may succeed (Ok(T)) or fail (Err(E)), used for error handling in file I/O, network requests, and parsing. The ? operator propagates errors or None automatically in functions returning Result or Option: it returns early on Err/None or unwraps the value to continue. Always prefer ? over unwrap() in production code to avoid panics.
How are Rust traits different from interfaces in other languages?
Rust traits are similar to interfaces but more powerful. Key differences: 1) Traits can have default method implementations; 2) You can implement your own trait for an external type (orphan rule: either the trait or the type must be defined in the current crate); 3) Trait objects (dyn Trait) support runtime polymorphism while generic bounds (T: Trait) provide zero-cost static dispatch; 4) Traits can be used as function parameters (impl Trait syntax) or return types. Important standard library traits include Clone, Copy, Debug, Display, Iterator, From/Into, and Serialize/Deserialize (via serde).
What is Tokio and how do I write async code in Rust?
Tokio is Rust's most popular async runtime, providing async I/O, task scheduling, timers, and network primitives. Rust's async/await syntax generates state machines (Futures), and the Tokio runtime polls and drives these Futures. Key concepts: async fn returns impl Future; .await suspends the current task until the Future completes; tokio::spawn creates concurrent tasks (similar to goroutines); tokio::select! waits on multiple Futures and takes the first to complete. Use the #[tokio::main] macro to initialize the runtime and run the async main function.
What advantages does Rust have over C++?
Rust's main advantages over C++: 1) Memory safety: the compiler eliminates dangling pointers, buffer overflows, and data races through ownership and borrow checking, whereas C++ relies on programmer discipline; 2) Concurrency safety: Send and Sync traits make data races impossible at compile time; 3) Better tooling: Cargo provides a unified package manager, build system, and test runner; 4) Superior error messages: Rust's compiler pinpoints exact problems with actionable suggestions; 5) No undefined behavior in safe Rust. Performance is comparable — both support zero-cost abstractions — but C++ has a more mature ecosystem and larger legacy codebase.
How do I build a web API in Rust?
The mainstream choices are Axum or Actix-web. Axum (tokio-rs/axum) is built on the Tower middleware ecosystem and Tokio, using an Extractor pattern to parse requests (Path, Query, Json), with routing defined via Router::new().route() — ideal for teams familiar with the Tower ecosystem. Actix-web is a high-performance actor-model framework that consistently ranks near the top in benchmarks, with an API style closer to traditional web frameworks. Both use serde for JSON serialization, sqlx or SeaORM for database access, and tower-http for CORS/logging/compression middleware.
Rust Core Concepts Quick Reference
Rust Core Concepts — Quick Reference
=====================================
Concept Syntax / Notes
------- --------------
Immutable variable let x = 5;
Mutable variable let mut x = 5;
Type annotation let x: i32 = 5;
Ownership transfer let y = x; // x is moved
Immutable borrow let r = &x;
Mutable borrow let r = &mut x; (only one at a time)
Dereference *r
String literal &str (reference, stack)
Owned string String (heap-allocated)
Option Some(val) | None
Result Ok(val) | Err(e)
Error propagation ? (replaces match Err(e) => return Err(e))
Struct struct Name { field: Type }
Tuple struct struct Color(u8, u8, u8);
Enum variant enum Msg { Quit, Move { x: i32 }, Write(String) }
Pattern match match val { Variant(x) => expr, _ => default }
Closure |args| body or |args| -> Type { body }
Generic function fn foo<T: Trait>(x: T) -> T
Lifetime annotation fn foo<'a>(x: &'a str) -> &'a str
Trait definition trait Name { fn method(&self) -> Type; }
Trait implementation impl TraitName for TypeName { ... }
Trait object Box<dyn Trait> / &dyn Trait
Async function async fn foo() -> T { ... }
Await let val = future.await;
Spawn task tokio::spawn(async { ... })
Arc (shared ownership) Arc::new(data) + Arc::clone(&arc)
Mutex (thread safety) Mutex::new(data) + data.lock().await
Vec Vec::new() / vec![1,2,3]
HashMap HashMap::new() + map.entry(k).or_insert(v)
Iterator chain iter.filter(|x| ...).map(|x| ...).collect()
Derive macros #[derive(Debug, Clone, Serialize, Deserialize)]
Test #[test] fn test_name() { assert_eq!(...); }
Conditional compile #[cfg(test)] mod tests { ... }