Rust for beginners

Rust for beginners

Rust is a systems programming language built for performance, memory safety, and explicit control. It gives beginners something unusual: low-level power without making memory bugs feel inevitable. The language is strict, but that strictness is the point. Rust pushes many mistakes into compile time, especially around ownership, mutation, and error handling, instead of letting them become crashes or corrupted state later .

This course moves in a practical order. It starts with the toolchain and first programs, then covers variables, types, and control flow. From there it moves into functions and expressions, then into the idea that defines Rust for most learners: ownership and borrowing. After that come structs, enums, collections, strings, and error handling, before everything is pulled together in a small command-line project.

Each practical section is paired with two kinds of reinforcement: flashcards for retention and exercises for hands-on skill. The goal is not just to read Rust, but to write it, break it, fix it, and start recognizing why the compiler is complaining.

Setting up Rust and writing your first programs

The beginner Rust toolchain has four names worth knowing immediately: rustup, rustc, cargo, and your editor.

Variables, types, and control flow

Most Rust programs begin with let. A binding created with let is immutable by default, which is one of Rust's first signals that state changes should be explicit rather than casual . If a variable must change, mark it with mut.

fn main() {
    let x = 5;
    let mut y = 10;

    y = y + 1;

    println!("x = {}, y = {}", x, y);
}

That distinction is small in syntax and large in mindset. In many beginner languages, mutability is assumed. In Rust, mutability is declared. That makes changing state visible.

Core types

Rust's beginner-level built-ins fall into a few useful groups:

  • Integers like i32 and u64

  • Floating-point numbers like f32 and f64

  • Booleans with true and false

  • Characters with char

  • Tuples for fixed-size grouped values

  • Arrays for fixed-size collections of the same type

Examples make the shape clearer:

fn main() {
    let age: i32 = 30;
    let pi: f64 = 3.14159;
    let is_ready: bool = true;
    let grade: char = 'A';

    let person = ("Mina", 24);
    let numbers = [10, 20, 30, 40];

    println!("{} is {}", person.0, person.1);
    println!("First number: {}", numbers[0]);
}

A tuple uses positions like .0 and .1. An array has a fixed length known at compile time. Rust often infers types, but explicit annotations help beginners read code and understand what the compiler expects.

Branching and repetition

Rust control flow is direct and readable. if branches on a boolean expression. loop repeats forever until broken. while repeats while a condition holds. for is the usual tool for iterating over collections or ranges .

fn main() {
    let number = 7;

    if number % 2 == 0 {
        println!("even");
    } else {
        println!("odd");
    }

    for n in 1..=3 {
        println!("{}", n);
    }
}

A few patterns are worth locking in

Functions and expressions

Rust functions are declared with fn, and their signatures make types explicit. Parameters are annotated, and return types are written after ->. This is part of Rust's broader preference for code that reveals intent at the boundary.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

This function teaches two things at once. First, parameters have declared types: a: i32, b: i32. Second, the final line has no semicolon, which means it is a trailing expression whose value becomes the return value.

Statements versus expressions

Rust is an expression-oriented language. Many constructs produce values. A statement does something. An expression evaluates to something.

Compare these:

fn main() {
    let x = 5; // statement

    let y = {
        let z = 3;
        z + 1
    }; // block expression

    println!("{}", y);
}

The block assigned to y evaluates to 4. If z + 1 had a semicolon, the block would evaluate to () instead. That single punctuation mark often causes confusion for beginners, so it is worth being explicit:

  • value; means "do this statement"

  • value means "this expression evaluates and can be returned"

Helper functions

Small helper functions keep Rust code readable. Instead of packing logic into main, split it into named pieces.

fn is_adult(age: u32) -> bool {
    age >= 18
}

fn describe_age(age: u32) -> &'static str {
    if is_adult(age) {
        "adult"
    } else {
        "minor"
    }
}

fn main() {
    println!("{}", describe_age(20));
}

This style matters later when code starts interacting with ownership and error handling. Beginners who learn early to separate logic into functions have a much easier time debugging and reasoning about programs.

A good checkpoint here is simple: the learner should be able to define a function, pass typed arguments into it, and predict whether the last line returns a value or ends a statement.

Ownership, borrowing, and references

Rust becomes much easier once ownership stops feeling like a punishment and starts feeling like a bookkeeping system for who is allowed to use what value, when.

This is the core of the language. Rust's ownership model gives each value a single owner. When ownership moves, the previous binding can no longer use that value. The point is memory safety without a garbage collector: values are cleaned up when their owner goes out of scope, and the compiler checks that code never uses freed data or creates unsafe aliasing .

Moves and clones

Types like integers are cheap to copy, but types like String own heap data. Assigning a String to a new variable usually moves it.

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

    // println!("{}", s1); // error: value moved
    println!("{}", s2);
}

After let s2 = s1;, s1 is no longer valid. That surprises beginners at first, but it prevents double frees and unclear ownership. If a real duplicate is needed, use clone().

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("{} {}", s1, s2);
}

The crucial distinction is this:

  • move transfers ownership

  • clone makes a deep copy

  • copy happens automatically only for simple Copy types like integers and booleans

Borrowing with references

Often code needs to use a value without taking ownership. That is what borrowing does. A reference points to data owned elsewhere.

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

fn main() {
    let name = String::from("rust");
    print_length(&name);
    println!("{}", name);
}

print_length borrows name with &String, so name remains usable afterward. This is shared, read-only access.

Rust allows either:

  • many immutable references, or

  • one mutable reference

but not both at the same time .

That rule is the center of borrowing:

fn main() {
    let mut text = String::from("hi");
    let r1 = &mut text;
    r1.push_str(" there");
    println!("{}", r1);
}

A mutable reference gives exclusive access. That exclusivity is why Rust can prevent data races at compile time. The compiler does not wait for runtime chaos. It rejects ambiguous simultaneous access patterns before the program runs .

Dereferencing and why the checker says no

A reference is not the owned value itself. It is an address-like handle to the value. In many everyday Rust examples, methods are called directly on references because the language applies dereferencing automatically where appropriate. Beginners still need the concept: ownership says who owns the data, borrowing says who may temporarily access it, and dereferencing is how a reference is treated as the underlying value when needed.

The borrow checker is strict for a reason. It prevents two notorious bug families:

  1. Dangling references: references to data that has already been dropped

  2. Aliased mutable access: multiple paths mutating the same data unsafely

The borrow checker is not trying to be clever. It is trying to make illegal states unrepresentable in ordinary Rust code.

This section is the hinge of the course. Once moves, borrows, clone, mutable references, and scope start to feel mechanical rather than mystical, the rest of the language becomes much easier to learn.

Structs, enums, and pattern matching

Once simple variables stop being enough, Rust needs richer data modeling tools. A struct groups related fields into one named type. An enum defines a type that can be one of several variants. Together, they let programs express shape, state, and possibility clearly .

A beginner struct might look like this:

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

fn main() {
    let user1 = User {
        username: String::from("mina"),
        active: true,
        sign_in_count: 1,
    };

    println!("{}", user1.username);
}

A struct says, "this record has these fields." It is the right tool when all pieces exist together.

Enums as labeled possibilities

An enum says, "this value can be one of several forms." The most important beginner example is Option<T>, which represents presence or absence instead of using null.

fn main() {
    let some_number = Some(5);
    let no_number: Option<i32> = None;
}

Another key enum is Result<T, E>, which represents success or failure. That becomes central in error handling, but the type idea starts here: Rust uses enums to model branching states directly.

Pattern matching

Enums become powerful with match. Pattern matching lets code branch based on the exact variant.

fn describe(value: Option<i32>) {
    match value {
        Some(n) => println!("Got {}", n),
        None => println!("Got nothing"),
    }
}

match must be exhaustive. Every possible case must be handled. That can feel strict, but it is one reason Rust programs are so explicit.

For simpler cases, if let is a compact alternative:

fn main() {
    let value = Some(10);

    if let Some(n) = value {
        println!("{}", n);
    }
}

A practical way to think about the relationship is this:

Learners should leave this section able to define custom types, construct values from them, and destructure those values in readable control flow.

Collections, strings, and iteration

Real programs rarely work with one value at a time. Rust's beginner collection types solve three common problems:

Error handling with Result and Option

Rust treats absence and failure as values to handle explicitly. Instead of null pointers and unchecked exceptions, beginner Rust code usually works with Option<T> for "maybe there is a value" and Result<T, E> for "this operation may succeed or fail" .

Option<T> is the right tool when missing data is normal:

fn first_item(values: &[i32]) -> Option<i32> {
    if values.is_empty() {
        None
    } else {
        Some(values[0])
    }
}

Result<T, E> is the right tool when an operation can fail and the reason matters:

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

match, unwrap, expect, and ?

The safest beginner habit is to handle these types with match.

fn main() {
    let result = divide(10.0, 2.0);

    match result {
        Ok(value) => println!("Result: {}", value),
        Err(message) => println!("Error: {}", message),
    }
}

Rust also provides shortcuts:

-

A small command-line project

The right beginner project is small enough to finish and rich enough to force several Rust ideas to work together. A number guessing game, a to-do list, or a tiny text analyzer all fit. What matters is not originality. What matters is combining the course pieces into one coherent program.

A good project workflow looks like this:

  1. Create the project with Cargo.

  2. Define the program state. Use variables, then upgrade to a struct or enum when the data stops fitting plain bindings.

  3. Write helper functions. Keep main readable.

  4. Handle user input and parsing. This brings in strings and Result.

  5. Store data in collections. Use Vec<T> or HashMap<K, V> as needed.

  6. Loop until the program is done.

  7. Handle failure explicitly. Avoid panic-driven design.

A tiny to-do list, for example, might use:

  • a struct Task for each item

  • a Vec<Task> for storage

  • a loop for the command prompt

  • functions like add_task, list_tasks, and complete_task

  • Result when parsing menu choices or indexes

  • borrowed references when displaying tasks without taking ownership

Here is the important shift: concepts that looked isolated in earlier sections stop being isolated in a project. Ownership affects how strings move into a collection. Enums affect how commands are represented. Functions affect how logic is organized. Error handling affects whether bad input crashes the program or produces a useful message.

Small projects are where Rust stops being a list of rules and starts becoming a programming language.

After this course, the next practical steps are clear:

  • Modules for splitting code across files

  • Traits for shared behavior

  • Lifetimes for more advanced borrowing relationships

  • Testing with Rust's built-in test tools

  • Crates from the broader Rust ecosystem for real-world functionality

The first milestone is not mastery. It is being able to open a blank Cargo project and build something that compiles, runs, and handles data deliberately.