Rust Fundamentals
Ownership, types, traits, error handling, I/O, threading, locking, channels, and async
Rust memory model (compile-time, zero runtime cost) Value ──owns──► Heap/Stack data │ ├── moved ──► new owner (old binding invalid) ├── &T ──► shared reference (many readers, no writers) ├── &mut T ──► exclusive reference (one writer, no readers) └── dropped ──► destructor called, memory freed Thread safety Send ──► value can be transferred to another thread Sync ──► &T can be shared across threads Arc<Mutex<T>> ──► shared ownership + interior mutability across threads
Ownership & BorrowingCore

Every value has exactly one owner. When the owner goes out of scope the value is dropped. References let you borrow a value without taking ownership — the borrow checker enforces the aliasing rules at compile time with zero runtime cost.

Move semantics

Rust
let s1 = String::from("hello");
let s2 = s1;           // s1 is moved — s1 is now invalid
// println!("{}", s1); // compile error: value used after move

let s3 = s2.clone();   // deep copy — both s2 and s3 are valid

// Types that implement Copy are duplicated automatically:
let x: i32 = 5;
let y = x;             // x is still valid — i32 is Copy

References and borrowing rules

At any given time you can have either any number of &T (shared) references, or exactly one &mut T (exclusive) reference — never both simultaneously.
Rust
fn length(s: &String) -> usize {
    s.len()            // borrows s; caller keeps ownership
}

fn append(s: &mut String) {
    s.push_str(" world");
}

let mut msg = String::from("hello");
append(&mut msg);
println!("{}", length(&msg));  // "hello world"

// Slice references: lightweight view into a collection
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3];  // [2, 3]

Ownership in functions

Rust
// Takes ownership — caller can no longer use s
fn consume(s: String) { println!("{}", s); }

// Returns ownership back
fn give_back(s: String) -> String { s }

// Preferred: borrow rather than take/return
fn process(s: &str) -> usize { s.len() }
Types & TraitsTypes

Rust is statically typed with inference. Structs hold data; traits define shared behaviour (like interfaces, but with no inheritance). Generics let you write code that works over many concrete types without runtime overhead.

Primitive types

CategoryTypesNotes
Integersi8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
Default inferred as i32. usize is pointer-sized.
Floatsf32 f64Default inferred as f64.
Booleanbooltrue / false
CharactercharUnicode scalar value (4 bytes).
Tuple(i32, &str, bool)Fixed-size, heterogeneous. Access with .0, .1
Array[i32; 5]Fixed-size, stack-allocated.
Slice&[T]Dynamically sized view into a sequence.
StringString / &strString = heap-owned. &str = borrowed string slice.

Structs

Rust
struct Point { x: f64, y: f64 }

impl Point {
    fn new(x: f64, y: f64) -> Self { Point { x, y } }
    fn distance(&self, other: &Point) -> f64 {
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }
}

// Tuple struct
struct Meters(f64);

// Unit struct (useful as a marker type)
struct Marker;

Traits

Rust
trait Area {
    fn area(&self) -> f64;
    fn describe(&self) -> String {          // default implementation
        format!("area = {:.2}", self.area())
    }
}

struct Circle { radius: f64 }
struct Rect   { w: f64, h: f64 }

impl Area for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Area for Rect {
    fn area(&self) -> f64 { self.w * self.h }
}

// Trait bounds — monomorphised (static dispatch, no overhead)
fn print_area<T: Area>(shape: &T) { println!("{}", shape.describe()); }

// Dynamic dispatch — erases concrete type at runtime
fn print_dyn(shape: &dyn Area) { println!("{}", shape.describe()); }

// Common standard traits
// Debug, Display, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord
#[derive(Debug, Clone, PartialEq)]
struct Config { timeout_ms: u64 }

Generics

Rust
struct Stack<T> { items: Vec<T> }

impl<T> Stack<T> {
    fn new() -> Self { Stack { items: Vec::new() } }
    fn push(&mut self, item: T) { self.items.push(item); }
    fn pop(&mut self) -> Option<T> { self.items.pop() }
}

// Where clauses for complex bounds
fn largest<T>(list: &[T]) -> &T
where T: PartialOrd
{
    let mut largest = &list[0];
    for item in list { if item > largest { largest = item; } }
    largest
}
Enums & Pattern MatchingTypes

Rust enums are algebraic data types — each variant can carry different data. Pattern matching with match is exhaustive: the compiler forces you to handle every variant.

Defining enums

Rust
#[derive(Debug)]
enum Message {
    Quit,                        // unit variant
    Move { x: i32, y: i32 },    // struct-like variant
    Write(String),               // tuple variant
    Color(u8, u8, u8),           // tuple variant (RGB)
}

impl Message {
    fn call(&self) {
        match self {
            Message::Quit           => println!("quit"),
            Message::Move { x, y } => println!("move to {x},{y}"),
            Message::Write(text)   => println!("write: {text}"),
            Message::Color(r,g,b)  => println!("color #{r:02x}{g:02x}{b:02x}"),
        }
    }
}

Option and Result

Rust
// Option<T> — value may or may not exist
let some: Option<i32> = Some(42);
let none: Option<i32> = None;

let val = some.unwrap_or(0);          // 42
let val = none.unwrap_or_default();   // 0
let val = some.map(|v| v * 2);       // Some(84)

// if let — match a single variant
if let Some(n) = some { println!("got {n}"); }

// while let
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() { println!("{top}"); }

// Result<T, E> — success or error
let ok: Result<i32, &str> = Ok(1);
let err: Result<i32, &str> = Err("oops");

match ok {
    Ok(v)  => println!("success: {v}"),
    Err(e) => eprintln!("error: {e}"),
}

Advanced patterns

Rust
let num = 7;
match num {
    1          => println!("one"),
    2 | 3      => println!("two or three"),
    4..=6      => println!("four to six"),
    n if n > 6 => println!("greater than six: {n}"),  // guard
    _          => println!("other"),
}

// Destructuring tuples and structs
let (a, b, c) = (1, 2, 3);
let Point { x, y } = Point::new(3.0, 4.0);

// @ bindings — capture while matching
match num {
    n @ 1..=10 => println!("small: {n}"),
    n          => println!("large: {n}"),
}
Error HandlingCore

Rust has no exceptions. Recoverable errors use Result<T, E>; unrecoverable bugs use panic!. The ? operator short-circuits on Err, propagating it to the caller.

The ? operator

Rust
use std::fs;
use std::io;

fn read_username() -> Result<String, io::Error> {
    let s = fs::read_to_string("/etc/hostname")?;  // returns Err early
    Ok(s.trim().to_string())
}

// Chain multiple fallible calls cleanly
fn pipeline() -> Result<u64, io::Error> {
    let content = fs::read_to_string("data.txt")?;
    let count = content.lines().count() as u64;
    Ok(count)
}

Custom error types

Rust
use std::fmt;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(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(s) => write!(f, "parse error: {s}"),
        }
    }
}
impl std::error::Error for AppError {}

// Auto-convert io::Error into AppError with From
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

// Now ? converts io::Error -> AppError automatically
fn load(path: &str) -> Result<String, AppError> {
    let s = std::fs::read_to_string(path)?;  // io::Error -> AppError::Io
    Ok(s)
}

// For quick prototyping — Box<dyn Error> accepts any error type
fn quick() -> Result<(), Box<dyn std::error::Error>> {
    let _s = std::fs::read_to_string("x.txt")?;
    Ok(())
}
Tip: Use the anyhow crate for application-level error propagation and thiserror for library error types — both eliminate the boilerplate above.
Collections & IteratorsCore

The standard collections live in std::collections. Iterators are lazy pipelines: chaining adaptors costs nothing until you collect or consume them.

Common collections

Rust
use std::collections::{HashMap, HashSet, BTreeMap, VecDeque};

// Vec — growable array
let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);
v.extend([5, 6]);
let first = v.first();           // Option<&i32>
v.retain(|&x| x % 2 == 0);     // keep evens: [2, 4, 6]

// HashMap — O(1) average lookup
let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.entry("Bob".to_string()).or_insert(0);  // insert if absent
let score = scores.get("Alice");               // Option<&u32>

// HashSet — unique elements, O(1) membership
let mut seen: HashSet<i32> = HashSet::new();
seen.insert(1); seen.insert(2); seen.insert(1);  // {1, 2}

// VecDeque — efficient push/pop from both ends
let mut dq: VecDeque<i32> = VecDeque::new();
dq.push_back(1); dq.push_front(0); dq.pop_front();  // [1]

Iterator adaptors

Rust
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];

// Lazy pipeline — nothing runs until collect/sum/etc.
let result: Vec<i32> = numbers.iter()
    .filter(|&&x| x % 2 == 0)       // [2, 4, 6, 8]
    .map(|&x| x * x)                // [4, 16, 36, 64]
    .take(3)                         // [4, 16, 36]
    .collect();

// Aggregations
let sum: i32  = numbers.iter().sum();
let max       = numbers.iter().max();     // Option<&i32>
let count     = numbers.iter().filter(|&&x| x > 4).count();

// flat_map — flatten one level of nesting
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter().flat_map(|s| s.split(' ')).collect();

// zip — pair two iterators
let pairs: Vec<_> = vec![1,2,3].into_iter().zip(["a","b","c"]).collect();

// enumerate — (index, value) pairs
for (i, v) in numbers.iter().enumerate() {
    println!("{i}: {v}");
}

// Custom iterator
struct Counter { count: u32, max: u32 }
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.count < self.max { self.count += 1; Some(self.count) }
        else { None }
    }
}
Closures & LifetimesCore

Closures capture their environment by reference or by value. Lifetimes are compile-time annotations that tell the borrow checker how long references stay valid — they never affect runtime.

Closures

Rust
// Fn trait hierarchy:
// FnOnce — can be called once (captures by move or drops captured values)
// FnMut  — can be called multiple times with &mut self
// Fn     — can be called multiple times with &self

let factor = 3;
let multiply = |x| x * factor;   // captures factor by reference (Fn)
println!("{}", multiply(5));       // 15

// move closure — takes ownership of captured values
let greeting = String::from("hello");
let greet = move || println!("{greeting}");  // greeting moved in

// Higher-order functions
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n }

Lifetimes

Rust
// Lifetime annotations say: "the returned reference lives as long as both inputs"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Structs holding references need lifetime annotations
struct Excerpt<'a> { part: &'a str }

// 'static — lives for the entire program duration
let s: &'static str = "I live forever";

// Elision rules — compiler infers lifetimes in common patterns
// fn first_word(s: &str) -> &str  infers  fn first_word<'a>(s: &'a str) -> &'a str
Smart PointersMemory

Smart pointers add ownership semantics or reference counting on top of raw heap allocation. Box is the simplest; Rc/Arc enable shared ownership; RefCell/Mutex enable interior mutability.

TypeOwnershipThread safeUse when
Box<T>SingleYes (if T: Send)Heap allocation, recursive types, trait objects
Rc<T>SharedNoMultiple owners in single-threaded code
Arc<T>SharedYesMultiple owners across threads
Cell<T>SingleNoInterior mutability for Copy types
RefCell<T>SingleNoInterior mutability with runtime borrow checks
Mutex<T>SharedYesExclusive mutable access across threads
RwLock<T>SharedYesMany readers or one writer across threads
Rust
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::Arc;

// Box: heap-allocate a value
let b = Box::new(5);

// Recursive type only possible with Box
enum List { Cons(i32, Box<List>), Nil }

// Rc: multiple owners in one thread
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared);
println!("refs: {}", Rc::strong_count(&shared));  // 2

// RefCell: borrow-check at runtime (panics on violation)
let data = Rc::new(RefCell::new(vec![1]));
data.borrow_mut().push(2);  // mutate through shared Rc

// Arc: Rc but atomic (cross-thread)
let arc = Arc::new(vec![1, 2, 3]);
let arc2 = Arc::clone(&arc);
std::thread::spawn(move || println!("{:?}", arc2));
File I/OI/O

std::fs covers one-shot reads/writes. std::io::{BufReader, BufWriter} add buffering for streaming. Files implement Read and Write traits — the same interface used for sockets and in-memory buffers.

Reading files

Rust
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Read};

// Entire file into String (small files)
let content = fs::read_to_string("config.txt")?;

// Entire file into Vec<u8> (binary)
let bytes = fs::read("image.png")?;

// Line-by-line (memory-efficient for large files)
let file = File::open("data.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
    let line = line?;
    println!("{line}");
}

// Read into a fixed buffer
let mut file = File::open("data.bin")?;
let mut buf = [0u8; 4096];
loop {
    let n = file.read(&mut buf)?;
    if n == 0 { break; }
    // process &buf[..n]
}

Writing files

Rust
use std::fs::{self, OpenOptions};
use std::io::{BufWriter, Write};

// Overwrite / create
fs::write("out.txt", "hello\n")?;

// Buffered write (flush on drop)
let file = fs::File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "line 1")?;
writeln!(writer, "line 2")?;
writer.flush()?;

// Append mode
let mut file = OpenOptions::new()
    .append(true).create(true).open("log.txt")?;
writeln!(file, "new line")?;

// Seek (random access)
use std::io::Seek;
let mut file = fs::File::open("data.bin")?;
file.seek(std::io::SeekFrom::Start(64))?;  // jump to byte 64

Directory and path operations

Rust
use std::path::Path;

fs::create_dir_all("logs/2026")?;

for entry in fs::read_dir(".")? {
    let entry = entry?;
    let path = entry.path();
    if path.extension().map(|e| e == "rs").unwrap_or(false) {
        println!("{}", path.display());
    }
}

let p = Path::new("/etc/hosts");
println!("{:?}", p.file_name());   // Some("hosts")
println!("{:?}", p.parent());      // Some("/etc")
println!("{}", p.exists());        // true/false
Network / Socket I/OI/O

std::net provides synchronous TCP and UDP sockets. For production async networking, use Tokio's TcpListener/TcpStream (see the Async section). Both implement the same Read/Write traits as files.

TCP server and client (sync)

Rust
use std::net::{TcpListener, TcpStream};
use std::io::{BufRead, BufReader, Write};
use std::thread;

// Server: accept connections in a loop
fn run_server() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080")?;
    println!("listening on :8080");
    for stream in listener.incoming() {
        let stream = stream?;
        thread::spawn(move || handle(stream));
    }
    Ok(())
}

fn handle(mut stream: TcpStream) {
    let reader = BufReader::new(stream.try_clone().unwrap());
    for line in reader.lines().flatten() {
        let response = format!("echo: {line}\n");
        stream.write_all(response.as_bytes()).ok();
    }
}

// Client: connect and exchange data
fn run_client() -> std::io::Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:8080")?;
    stream.write_all(b"hello\n")?;
    let mut resp = String::new();
    BufReader::new(&stream).read_line(&mut resp)?;
    println!("server said: {resp}");
    Ok(())
}

UDP socket

Rust
use std::net::UdpSocket;

// Sender
let sock = UdpSocket::bind("0.0.0.0:0")?;     // OS picks port
sock.send_to(b"ping", "127.0.0.1:9000")?;

// Receiver
let sock = UdpSocket::bind("0.0.0.0:9000")?;
let mut buf = [0u8; 1024];
let (n, src) = sock.recv_from(&mut buf)?;
println!("from {src}: {:?}", &buf[..n]);

// Set timeouts to avoid blocking forever
use std::time::Duration;
sock.set_read_timeout(Some(Duration::from_secs(5)))?;

Socket options

Rust
use std::time::Duration;

stream.set_nodelay(true)?;                        // disable Nagle
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(30)))?;
stream.set_ttl(64)?;
let addr = stream.peer_addr()?;
ThreadingConcurrency

Rust's ownership system makes data-race freedom a compile-time guarantee: a value can only be sent to another thread if it implements Send; a shared reference can only cross a thread boundary if T: Sync. Closures capture their environment, so move closures are the standard way to hand off data.

Spawning threads

Rust
use std::thread;
use std::time::Duration;

// Spawn and join
let handle = thread::spawn(|| {
    println!("running in thread {:?}", thread::current().id());
    42
});
let result = handle.join().unwrap();  // blocks until thread finishes; returns 42

// move closure — take ownership of data before crossing thread boundary
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("{:?}", data);   // data moved into thread
});
h.join().unwrap();

// Thread builder: name and stack size
let h = thread::Builder::new()
    .name("worker-1".into())
    .stack_size(4 * 1024 * 1024)
    .spawn(|| { /* ... */ })?;
h.join().unwrap();

// Thread-local storage
thread_local! {
    static COUNTER: std::cell::Cell<u32> = std::cell::Cell::new(0);
}
COUNTER.with(|c| c.set(c.get() + 1));

Scoped threads (std::thread::scope)

Rust
// Scoped threads can borrow from the outer stack frame safely
let data = vec![1, 2, 3, 4];
thread::scope(|s| {
    s.spawn(|| println!("first half:  {:?}", &data[..2]));
    s.spawn(|| println!("second half: {:?}", &data[2..]));
});
// All threads joined here — data is still accessible below
println!("all done, data len = {}", data.len());
Locking & SynchronisationConcurrency

Mutex<T> and RwLock<T> wrap data and only let you access it after acquiring the lock. The lock guard is released automatically when it goes out of scope — no risk of forgetting to unlock. Wrap in Arc to share across threads.

Mutex

Rust
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0_u64));

let handles: Vec<_> = (0..8).map(|_| {
    let c = Arc::clone(&counter);
    thread::spawn(move || {
        let mut guard = c.lock().unwrap();  // blocks until acquired
        *guard += 1;
        // guard dropped here — lock released
    })
}).collect();

for h in handles { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap());  // 8

// Poisoning: if a thread panics while holding the lock,
// subsequent lock() calls return Err(PoisonError). Recover with:
let guard = counter.lock().unwrap_or_else(|e| e.into_inner());

RwLock — many readers or one writer

Rust
use std::sync::{Arc, RwLock};

let cache: Arc<RwLock<HashMap<String, String>>> =
    Arc::new(RwLock::new(HashMap::new()));

// Multiple concurrent readers
let r = cache.read().unwrap();
println!("{:?}", r.get("key"));
drop(r);   // release read lock before taking write lock

// Exclusive writer
let mut w = cache.write().unwrap();
w.insert("key".to_string(), "value".to_string());

Atomics — lock-free primitives

Rust
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

let count = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&count);
thread::spawn(move || {
    c.fetch_add(1, Ordering::SeqCst);
});

// Ordering guide:
// Relaxed  — no synchronisation, just atomicity (counters, stats)
// Acquire  — read side of a happens-before relationship
// Release  — write side of a happens-before relationship
// SeqCst   — total sequential order (safest, most expensive)

// AtomicBool is common for stop-flags
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = Arc::clone(&running);
thread::spawn(move || {
    while r.load(Ordering::Acquire) { /* work */ }
});
running.store(false, Ordering::Release);

Condvar — wait for a condition

Rust
use std::sync::{Arc, Condvar, Mutex};

let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = Arc::clone(&pair);

thread::spawn(move || {
    let (lock, cvar) = &*pair2;
    let mut ready = lock.lock().unwrap();
    *ready = true;
    cvar.notify_one();
});

let (lock, cvar) = &*pair;
let mut ready = lock.lock().unwrap();
while !*ready {
    ready = cvar.wait(ready).unwrap();
}
println!("signalled!");
ChannelsConcurrency

std::sync::mpsc (multiple-producer, single-consumer) is the standard way to pass ownership of data between threads without shared state. For multi-consumer or bounded patterns, the crossbeam-channel or flume crates provide richer options.

mpsc — unbounded channel

Rust
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel::<String>();

// Multiple producers from cloned senders
for i in 0..4 {
    let tx = tx.clone();
    thread::spawn(move || {
        tx.send(format!("message from thread {i}")).unwrap();
    });
}
drop(tx);  // drop the original sender so receiver knows when all senders are gone

// Consumer drains until all senders dropped
for msg in rx {
    println!("{msg}");
}

// Non-blocking try_recv
match rx.try_recv() {
    Ok(msg)                                => println!("{msg}"),
    Err(mpsc::TryRecvError::Empty)       => { /* nothing yet */ }
    Err(mpsc::TryRecvError::Disconnected) => { /* all senders gone */ }
}

mpsc — sync (bounded) channel

Rust
// sync_channel(n): sender blocks when buffer is full (back-pressure)
let (tx, rx) = mpsc::sync_channel::<u32>(16);  // capacity = 16

thread::spawn(move || {
    for i in 0..100 {
        tx.send(i).unwrap();  // blocks when buffer full
    }
});

for val in rx { print!("{val} "); }

// Worker pool pattern: one channel, N worker threads
let (tx, rx) = mpsc::channel::<u32>();
let rx = Arc::new(Mutex::new(rx));
for _ in 0..4 {
    let rx = Arc::clone(&rx);
    thread::spawn(move || {
        loop {
            let job = rx.lock().unwrap().recv();
            match job {
                Ok(n)  => println!("worker processing {n}"),
                Err(_) => break,  // sender disconnected
            }
        }
    });
}
Async / TokioAsync

Rust's async/await syntax produces state machines that are polled to completion by an executor. Tokio is the de-facto async runtime — it provides an I/O reactor, a work-stealing thread pool, timers, and async-native TCP/UDP/file utilities.

Add to Cargo.toml:
tokio = { version = "1", features = ["full"] }

The basics

Rust
use tokio::time::{sleep, Duration};

// async fn returns a Future — it runs when awaited
async fn fetch(id: u32) -> String {
    sleep(Duration::from_millis(100)).await;  // yields, doesn't block thread
    format!("result for {id}")
}

// #[tokio::main] creates the runtime and runs async main
#[tokio::main]
async fn main() {
    let result = fetch(1).await;
    println!("{result}");
}

// Run futures concurrently with join!
let (a, b) = tokio::join!(fetch(1), fetch(2));

// Spawn onto the executor (like thread::spawn but async)
let task = tokio::spawn(async { fetch(3).await });
let value = task.await.unwrap();

Async TCP server

Rust
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;
    loop {
        let (stream, addr) = listener.accept().await?;
        println!("connection from {addr}");
        tokio::spawn(async move { handle(stream).await });
    }
}

async fn handle(mut stream: TcpStream) {
    let (reader, mut writer) = stream.split();
    let mut lines = BufReader::new(reader).lines();
    while let Ok(Some(line)) = lines.next_line().await {
        let resp = format!("echo: {line}\n");
        writer.write_all(resp.as_bytes()).await.ok();
    }
}

Async channels and select!

Rust
use tokio::sync::{mpsc, broadcast};
use tokio::time::{sleep, Duration};

// mpsc — async multiple-producer single-consumer
let (tx, mut rx) = mpsc::channel::<u32>(32);
tokio::spawn(async move { tx.send(1).await.unwrap(); });
while let Some(v) = rx.recv().await { println!("{v}"); }

// broadcast — one sender, many receivers (each gets every message)
let (btx, mut brx) = broadcast::channel::<u32>(16);
let mut brx2 = btx.subscribe();
btx.send(42).unwrap();

// select! — race multiple async branches
tokio::select! {
    val = rx.recv()              => println!("channel: {val:?}"),
    _ = sleep(Duration::from_secs(1)) => println!("timeout"),
}

Shared state in async code

Rust
use tokio::sync::{Mutex, RwLock};
use std::sync::Arc;

// tokio::sync::Mutex — async-aware, holds lock across .await
let state = Arc::new(Mutex::new(Vec::<u32>::new()));
let s = Arc::clone(&state);
tokio::spawn(async move {
    let mut guard = s.lock().await;
    guard.push(1);
});

// Prefer std::sync::Mutex when the lock is never held across .await
// (avoids the async overhead for synchronous critical sections)

// OnceLock / LazyLock for one-time async initialisation
use tokio::sync::OnceCell;
static CONFIG: OnceCell<String> = OnceCell::const_new();
let cfg = CONFIG.get_or_init(|| async { "loaded".to_string() }).await;
Cheat SheetReference

Ownership quick rules

Move by default for non-Copy types
&T — many shared borrows
&mut T — one exclusive borrow
clone() — explicit deep copy
Value dropped when owner leaves scope

Error propagation

? — propagate Err/None up
unwrap() — panic on Err/None
unwrap_or(v) — default value
map_err(|e| ...) — convert error
anyhow::Result — app errors
thiserror — library errors

Common traits to implement

Display — user-facing formatting
Debug — derive, for {:?}
From / Into — conversions
Iterator — custom iterators
Default — zero value
Drop — custom destructor

Concurrency patterns

Arc<Mutex<T>> — shared mutable state
Arc<RwLock<T>> — read-heavy shared state
mpsc::channel — pass data between threads
AtomicBool — stop flags
Prefer channels over shared state

File I/O patterns

fs::read_to_string() — small text files
BufReader::lines() — large text files
BufWriter — batch writes
OpenOptions — append / create flags
fs::read() — binary files

Async rules of thumb

tokio::spawn — background task
tokio::join! — concurrent, same scope
tokio::select! — race futures
Don't block inside async — use spawn_blocking
Use tokio::sync::Mutex if held across .await

Smart pointer decision tree

Single owner → Box<T>
Shared, one thread → Rc<T>
Shared, multi-thread → Arc<T>
Interior mutability (single) → RefCell<T>
Interior mutability (multi) → Mutex<T> / RwLock<T>

Cargo essentials

cargo new <name> — new project
cargo build --release — optimised build
cargo test — run tests
cargo clippy — lint
cargo doc --open — browse docs
cargo add <crate> — add dependency

Type conversions at a glance

FromToHow
&strStrings.to_string() or String::from(s)
String&str&s or s.as_str()
i32Stringn.to_string()
&stri32s.parse::<i32>()?
Vec<T>&[T]&v or v.as_slice()
&[T]Vec<T>slice.to_vec()
Option<T>Result<T, E>opt.ok_or(err)
Result<T, E>Option<T>result.ok()

Iterator cheat sheet

AdaptorWhat it does
map(f)Transform each element
filter(pred)Keep elements matching predicate
filter_map(f)Map then discard None
flat_map(f)Map then flatten one level
take(n) / skip(n)Limit / skip first n elements
enumerate()Pair each element with its index
zip(other)Pair elements from two iterators
chain(other)Concatenate two iterators
fold(init, f)Accumulate into a single value
any(pred) / all(pred)Short-circuit boolean checks
collect::<C>()Consume into a collection
sum() / product()Numeric aggregations
min() / max()Smallest / largest element (Option)
count()Number of elements
peekable()Look at next element without consuming