COURSE · 6 LESSONS · 100% FREE
🦀

Rust Programming

Memory safe, blazing fast — 6 lessons covering ownership, traits, error handling, concurrency, and unsafe Rust. Build fearless concurrent systems.

0Lessons
0Code Examples
SomeProgramming Experience
0%
You've completed 0 of 6 lessons
J/ Next
K/ Prev
Esc Collapse
/ Search
Core Concepts

Ownership Rules

Every value has exactly one owner. When the owner goes out of scope, the value is dropped. This eliminates garbage collectors, data races, and dangling pointers at compile time.

Rust
fn main() {
    // Rule 1: Each value has exactly one owner
    let s1 = String::from("hello");

    // Rule 2: When the owner goes out of scope, the value is dropped
    {
        let s2 = String::from("inside block");
        println!("{}", s2); // s2 is valid here
    }
    // s2 is dropped here — memory freed automatically

    // Rule 3: Assignment transfers ownership (move)
    let s3 = s1; // s1 is MOVED to s3
    // println!("{}", s1); // ERROR: s1 is no longer valid!
    println!("{}", s3); // s3 owns the data now
}

Clone & Copy

Rust
fn main() {
    // Clone — deep copy of heap data
    let s1 = String::from("hello");
    let s2 = s1.clone(); // both s1 and s2 are valid
    println!("s1 = {}, s2 = {}", s1, s2);

    // Copy types — stack-only, trivially copyable
    let x = 42;
    let y = x; // Copy, not move
    println!("x = {}, y = {}", x, y); // both valid!

    // Types that implement Copy:
    // i8, i16, i32, i64, i128, isize
    // u8, u16, u32, u64, u128, usize
    // f32, f64
    // bool, char
    // Tuples of Copy types: (i32, f64)
}

Borrowing & References

Rust
fn main() {
    let s1 = String::from("hello");

    // Immutable borrow — can have many simultaneously
    let len = calculate_length(&s1);
    println!("Length of '{}' is {}", s1, len);

    // Mutable borrow — only ONE at a time
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("Modified: {}", s2);
}

fn calculate_length(s: &String) -> usize {
    s.len()
    // s goes out of scope but doesn't drop the original
}

fn change(s: &mut String) {
    s.push_str(", world");
}
💡
Ownership Rule Rust enforces the rule: at any given time, you can have either ONE mutable reference OR any number of immutable references. This prevents data races at compile time.

Lifetimes

Rust
// Lifetime annotations tell the compiler how references relate
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 { 3 }

    fn announce_and_return(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence;
    {
        let i = novel.split('.').next().expect("Could not find a '.'");
        first_sentence = ImportantExcerpt { part: i };
    }
    println!("First sentence: {}", first_sentence.part);
}

&str vs String

Rust
fn main() {
    // String — heap-allocated, growable, owned
    let owned: String = String::from("I own this");

    // &str — string slice, borrowed view into string data
    let slice: &str = "I'm a string literal"; // lives in binary
    let slice2: &str = &owned[..]; // borrow of String

    // Conversions
    let from_str: &str = "hello";
    let to_string: String = from_str.to_string();

    let from_string: String = String::from("hello");
    let to_slice: &str = &from_string;

    // Function signatures — prefer &str for input
    fn greet(name: &str) {
        println!("Hello, {}!", name);
    }

    greet(&to_string); // String coerces to &str
    greet("literal");  // &str directly
}

Key Takeaways

  • Each value has exactly one owner — ownership moves on assignment
  • Clone for explicit deep copies, Copy for trivially copyable types
  • Borrowing lets you use values without taking ownership
  • One mutable reference OR many immutable references — never both
  • Prefer &str in function parameters for flexibility
🧪 Quick Check
What happens when you assign let s2 = s1; where s1 is a String?

Struct Basics

Rust
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

fn main() {
    // Create instance — all fields required
    let user = User {
        username: String::from("mayank"),
        email: String::from("mayank@example.com"),
        active: true,
        sign_in_count: 1,
    };

    // Field init shorthand (variable name == field name)
    let username = String::from("alice");
    let email = String::from("alice@example.com");
    let user2 = User {
        username,
        email,
        active: true,
        sign_in_count: 1,
    };

    // Struct update syntax
    let user3 = User {
        email: String::from("new@example.com"),
        ..user2 // remaining fields from user2
    };
}

Methods & Associated Functions

Rust
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // Method — takes &self
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    // Associated function — no self (like static methods)
    fn square(size: f64) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect = Rectangle { width: 30.0, height: 50.0 };
    println!("Area: {}", rect.area());

    let sq = Rectangle::square(20.0);
    println!("Square area: {}", sq.area());
}

Enums & Pattern Matching

Rust
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

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

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let msg = Message::Write(String::from("hello"));
    msg.call();
}

Option & Result

Rust
// Option replaces null — compiler forces you to handle it
fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some(String::from("Mayank")),
        _ => None,
    }
}

fn main() {
    let user = find_user(1);

    // match on Option
    match &user {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // unwrap_or for defaults
    let name = find_user(99).unwrap_or(String::from("Unknown"));
    println!("Name: {}", name);

    // map for transformations
    let upper = find_user(1)
        .map(|name| name.to_uppercase())
        .unwrap_or_default();
    println!("Upper: {}", upper);
}

// Result for error handling
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 3.0) {
        Ok(result) => println!("Result: {:.2}", result),
        Err(e) => println!("Error: {}", e),
    }
}
💡
Exhaustive Matching Use match exhaustively — the compiler ensures you handle every possible variant. The _ pattern catches everything else. This eliminates unhandled cases at compile time.

Key Takeaways

  • Structs group related data; enums represent variants with data
  • Use impl blocks for methods and associated functions
  • Option<T> replaces null — the compiler forces you to handle the absent case
  • Result<T, E> replaces exceptions — errors are values, not control flow
  • Pattern matching with match is exhaustive and compiler-checked
🧪 Quick Check
What does Option<T> replace in Rust?
← PrevOwnership & Borrowing

Generic Functions

Rust
// Generic function — works with any type
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in &list[1..] {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Largest: {}", largest(&numbers));

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("Largest: {}", largest(&chars));
}

Trait Definitions

Rust
trait Summary {
    // Required method — must be implemented
    fn summarize(&self) -> String;

    // Default implementation — optional override
    fn preview(&self) -> String {
        format!("{}...", &self.summarize()[..20])
    }
}

struct Article {
    title: String,
    author: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}, by {} — {}", self.title, self.author, &self.content[..50])
    }
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

fn main() {
    let article = Article {
        title: String::from("Rust is Great"),
        author: String::from("Mayank"),
        content: String::from("Rust provides memory safety without a garbage collector..."),
    };
    println!("{}", article.summarize());
}

Trait Bounds & impl Trait

Rust
use std::fmt::{Display, Debug};

// Trait bound syntax
fn print_info<T: Display + Debug>(item: &T) {
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
}

// impl Trait syntax — cleaner for simple cases
fn print_item(item: &impl Display) {
    println!("{}", item);
}

// Where clause — complex bounds
fn process<T, U>(t: &T, u: &U) -> String
where
    T: Display + Clone,
    U: Debug + PartialOrd,
{
    format!("{}: {:?}", t, u)
}

// Trait as return type
fn make_greeting(name: &str) -> impl Display {
    format!("Hello, {}!", name)
}

fn main() {
    print_info(&42);
    print_item(&"hello");
    println!("{}", process(&"test", &3.14));
}

Associated Types

Rust
trait Iterator {
    type Item; // associated type — one implementation per type
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Counter {
        Counter { count: 0, max }
    }
}

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
        }
    }
}

fn main() {
    let counter = Counter::new(5);
    let values: Vec<u32> = counter.collect();
    println!("{:?}", values); // [1, 2, 3, 4, 5]
}

Derive Macros

Rust
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

#[derive(Debug)]
enum Color {
    Red,
    Green,
    Blue,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1.clone();

    println!("{:?}", p1); // Point { x: 1.0, y: 2.0 }
    println!("Equal: {}", p1 == p2); // true

    let color = Color::Red;
    println!("{:?}", color); // Red
}
Zero-Cost Abstractions Rust uses monomorphization — generics are resolved at compile time, generating specialized code for each concrete type. Zero runtime cost compared to writing each type manually.

Key Takeaways

  • Traits define shared behavior — similar to interfaces
  • Use trait bounds (<T: Trait>) to constrain generics
  • Associated types fix the number of implementations (one Item per Iterator)
  • #[derive] auto-implements common traits like Debug, Clone, PartialEq
  • Zero-cost abstractions — generic code is as fast as hand-written type-specific code
← PrevStructs & Enums
Intermediate

Result Basics

Rust
use std::fs;
use std::num::ParseIntError;

fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?; // ? propagates errors
    let number = content.trim().parse::<i32>()?; // ? propagates parse errors
    Ok(number)
}

fn main() {
    match read_number("number.txt") {
        Ok(n) => println!("Number: {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

The ? Operator

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

// Without ? — verbose
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
    let file = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(e) => Err(e),
    }
}

// With ? — clean and idiomatic
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// Chained
fn read_file_chained(path: &str) -> Result<String, io::Error> {
    let mut contents = String::new();
    File::open(path)?.read_to_string(&mut contents)?;
    Ok(contents)
}

Custom Error Types

Rust
use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(String),
    PermissionDenied,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::NotFound(s) => write!(f, "Not found: {}", s),
            AppError::ParseError(s) => write!(f, "Parse error: {}", s),
            AppError::PermissionDenied => write!(f, "Permission denied"),
        }
    }
}

impl std::error::Error for AppError {}

fn find_config(name: &str) -> Result<String, AppError> {
    match name {
        "prod" => Ok(String::from("production config")),
        "dev" => Ok(String::from("development config")),
        _ => Err(AppError::NotFound(name.to_string())),
    }
}

fn main() {
    match find_config("staging") {
        Ok(config) => println!("{}", config),
        Err(AppError::NotFound(name)) => println!("Config '{}' not found", name),
        Err(e) => println!("Error: {}", e),
    }
}

anyhow & thiserror Crates

Rust
// Cargo.toml:
// [dependencies]
// anyhow = "1"
// thiserror = "1"

use anyhow::{Context, Result};
use thiserror::Error;

// thiserror — derive macro for custom errors
#[derive(Error, Debug)]
enum DatabaseError {
    #[error("Connection failed: {0}")]
    Connection(String),

    #[error("Query failed: {0}")]
    Query(String),

    #[error(transparent)]
    Other(#[from] std::io::Error),
}

// anyhow — ergonomic error handling for applications
fn read_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    Ok(content)
}

fn main() -> Result<()> {
    let config = read_config()?;
    println!("{}", config);
    Ok(())
}
💡
Crate Selection Use anyhow for applications (quick error handling with context) and thiserror for libraries (structured, typed errors that callers can match on).

Key Takeaways

  • Every function that can fail returns Result<T, E>
  • The ? operator propagates errors automatically — no try/catch
  • Custom errors need Display and Error trait implementations
  • thiserror for library errors, anyhow for application errors
  • .context() adds human-readable error messages to anyhow errors
← PrevTraits & Generics

Threads

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

fn main() {
    // Spawn a thread
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Spawned thread: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
    });

    for i in 1..=3 {
        println!("Main thread: {}", i);
        thread::sleep(Duration::from_millis(150));
    }

    handle.join().unwrap(); // wait for thread to finish

    // Move data into thread
    let names = vec!["Mayank", "Alice", "Bob"];
    let handle = thread::spawn(move || {
        for name in names {
            println!("Hello, {}!", name);
        }
    });
    handle.join().unwrap();
}

Arc & Mutex

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

fn main() {
    // Arc = Atomic Reference Counting (shared ownership)
    // Mutex = Mutual Exclusion (interior mutability)
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final count: {}", *counter.lock().unwrap()); // 10
}

Channels

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

fn main() {
    // mpsc = Multiple Producer, Single Consumer
    let (tx, rx) = mpsc::channel();

    // Producer 1
    let tx1 = tx.clone();
    thread::spawn(move || {
        let messages = vec!["hello", "from", "thread 1"];
        for msg in messages {
            tx1.send(msg.to_string()).unwrap();
            thread::sleep(Duration::from_millis(200));
        }
    });

    // Producer 2
    thread::spawn(move || {
        let messages = vec!["hi", "from", "thread 2"];
        for msg in messages {
            tx.send(msg.to_string()).unwrap();
            thread::sleep(Duration::from_millis(300));
        }
    });

    // Receive all messages
    for received in rx {
        println!("Got: {}", received);
    }
}

Send & Sync Traits

Rust
use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // Send — can be transferred between threads
    // Sync — can be referenced from multiple threads
    // Arc is Send + Sync — safe for concurrent access
    let data = Arc::new(vec![1, 2, 3]);

    // Rc is NOT Send — can't move to another thread
    // let bad = Rc::new(5);
    // thread::spawn(move || println!("{}", bad)); // COMPILE ERROR!

    // Clone Arc for each thread
    let data1 = Arc::clone(&data);
    let data2 = Arc::clone(&data);

    let h1 = std::thread::spawn(move || println!("{:?}", data1));
    let h2 = std::thread::spawn(move || println!("{:?}", data2));

    h1.join().unwrap();
    h2.join().unwrap();
}

Rayon — Parallel Iterators

Rust
// Cargo.toml: rayon = "1"

use rayon::prelude::*;

fn main() {
    let numbers: Vec<u64> = (1..=10_000_000).collect();

    // Sequential
    let sum1: u64 = numbers.iter().sum();

    // Parallel — just change iter() to par_iter()
    let sum2: u64 = numbers.par_iter().sum();

    // Parallel map/filter
    let squares: Vec<u64> = (1..=1_000_000)
        .into_par_iter()
        .filter(|x| x % 2 == 0)
        .map(|x| x * x)
        .collect();

    println!("Sequential sum: {}", sum1);
    println!("Parallel sum: {}", sum2);
    println!("Even squares count: {}", squares.len());
}
Easy Parallelism Rayon uses work-stealing to balance load across threads. Switching from sequential to parallel is often just changing .iter() to .par_iter() — zero code restructuring needed.

Key Takeaways

  • Rust prevents data races at compile time via ownership + Send/Sync
  • Arc<Mutex<T>> is the standard pattern for shared mutable state
  • Channels (mpsc) enable message-passing concurrency
  • rayon makes parallelism trivial with parallel iterators
  • If it compiles, it's free of data races — the compiler guarantees it
🧪 Quick Check
Why can't you move an Rc<T> to another thread?
← PrevError Handling
Advanced

Raw Pointers

Rust
fn main() {
    let mut num = 42;

    // Creating raw pointers — allowed in safe code
    let r1 = &num as *const i32;   // immutable raw pointer
    let r2 = &mut num as *mut i32; // mutable raw pointer

    // Dereferencing requires unsafe
    unsafe {
        println!("r1: {}", *r1);
        println!("r2: {}", *r2);
        *r2 = 100;
        println!("num: {}", num);
    }

    // Creating a pointer from an address
    let address = 0x012345usize;
    let _r = address as *const i32;
    // Dereferencing an arbitrary address is undefined behavior!
}

Unsafe Superpowers

Rust
// Unsafe lets you do 5 things that safe Rust forbids:

// 1. Dereference a raw pointer
// 2. Call an unsafe function or method
// 3. Access or modify a mutable static variable
// 4. Implement an unsafe trait
// 5. Access union fields

// Example: splitting a slice
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    assert!(mid <= len);

    let ptr = slice.as_mut_ptr();
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut v, 3);
    println!("Left: {:?}, Right: {:?}", left, right);
}

FFI (Foreign Function Interface)

Rust
// Calling C functions from Rust
extern "C" {
    fn abs(input: i32) -> i32;
    fn sqrt(input: f64) -> f64;
}

// Exposing Rust functions to C
#[no_mangle]
pub extern "C" fn rust_function(x: i32) -> i32 {
    x * 2
}

fn main() {
    unsafe {
        println!("C abs(-5) = {}", abs(-5));
        println!("C sqrt(9.0) = {}", sqrt(9.0));
    }
}

// Using libc crate for more C bindings
// Cargo.toml: libc = "0.2"
use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn getenv(name: *const c_char) -> *mut c_char;
}

fn safe_getenv(name: &str) -> Option<String> {
    let c_name = CString::new(name).ok()?;
    unsafe {
        let ptr = getenv(c_name.as_ptr());
        if ptr.is_null() {
            None
        } else {
            let c_str = std::ffi::CStr::from_ptr(ptr);
            Some(c_str.to_string_lossy().into_owned())
        }
    }
}

When to Use Unsafe

Rust
// Safe abstraction over unsafe code — the Rust way
struct SafeBuffer {
    ptr: *mut u8,
    len: usize,
    capacity: usize,
}

impl SafeBuffer {
    fn new(capacity: usize) -> SafeBuffer {
        let layout = std::alloc::Layout::array::<u8>(capacity).unwrap();
        let ptr = unsafe { std::alloc::alloc(layout) };
        if ptr.is_null() {
            std::alloc::handle_alloc_error(layout);
        }
        SafeBuffer { ptr, len: 0, capacity }
    }

    // Safe public API — callers never touch unsafe
    fn push(&mut self, byte: u8) {
        if self.len == self.capacity {
            panic!("Buffer full");
        }
        unsafe {
            self.ptr.add(self.len).write(byte);
        }
        self.len += 1;
    }

    fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }
}

impl Drop for SafeBuffer {
    fn drop(&mut self) {
        let layout = std::alloc::Layout::array::<u8>(self.capacity).unwrap();
        unsafe {
            std::alloc::dealloc(self.ptr, layout);
        }
    }
}

fn main() {
    let mut buf = SafeBuffer::new(10);
    buf.push(b'H');
    buf.push(b'i');
    println!("{:?}", buf.as_slice()); // [72, 105]
}
🚫
Unsafe Best Practices Keep unsafe blocks as small as possible. Wrap unsafe code in safe abstractions with clear APIs. Document why the unsafe code is sound with comments like // SAFETY: ....

Key Takeaways

  • Unsafe is an escape hatch, not a feature to use casually
  • Raw pointers let you do manual memory manipulation
  • FFI enables calling C/C++ code and exposing Rust to other languages
  • Always wrap unsafe code in safe abstractions
  • Document the safety invariants that your unsafe code relies on
🧪 Quick Check
How many superpowers does unsafe unlock?
← PrevConcurrency

📚 Resources & Further Learning

📖
The Rust Book
The official "Programming Rust" book — the definitive learning resource.
doc.rust-lang.org/book →
📘
Rust by Example
Learn Rust through runnable code examples — hands-on approach.
doc.rust-lang.org/rust-by-example →
🎯
docs.rs
Auto-generated documentation for every published crate on crates.io.
docs.rs →
🧪
Rustlings
Small exercises to get you used to reading and writing Rust code.
github.com/rust-lang/rustlings →
The Rustonomicon
Unsafe Rust deep dive — advanced topics for systems programmers.
doc.rust-lang.org/nomicon →
📦
crates.io
The Rust package registry — discover and use community libraries.
crates.io →
AI
Rust Tutor
ZenMux · GLM 4.7 Flash
Ask me anything about Rust! I can help with ownership, lifetimes, trait puzzles, concurrency patterns, or explain any concept from the lessons above.