Back to Home

Installing & Learning Rust

Install the toolchain, write your first program, and learn the fundamentals of Rust — the language built for speed and memory safety without a garbage collector.

Prefer to learn by building? After this page, try the Learning to Code by Bowling (Rust) build-along — you'll write a real bowling scorer twice, the long way and the smart way.

Contents

  1. What is Rust?
  2. Installing Rust (rustup)
  3. Cargo — Your First Project
  4. Program Structure
  5. Variables & Data Types
  6. Operators
  7. Strings
  8. Arrays, Vectors, Tuples & Maps
  9. Control Flow & match
  10. Loops
  11. Functions
  12. Ownership & Borrowing
  13. Structs & Methods
  14. Enums, Option & Result
  15. Error Handling
  16. Traits & Generics

1. What is Rust?

Rust is a statically typed, compiled systems programming language focused on three things at once: performance, memory safety, and concurrency. It runs about as fast as C and C++, but a compile-time system called ownership catches whole classes of bugs — use-after-free, double-free, data races — before your program ever runs.

Rust asks more of you up front than most languages. The payoff is a program you can refactor fearlessly — the compiler re-checks every safety rule each time you build.

2. Installing Rust (rustup)

The official installer is rustup. It installs the compiler (rustc), the build tool (cargo), and manages updates and toolchain versions for you. Never install Rust from a distro package manager — rustup keeps everything current and self-contained in your home directory.

Linux & macOS

Open a terminal and run the one-line installer, then follow the prompts (press 1 for the standard install):

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

When it finishes, either restart your terminal or load the new environment into the current shell:

source "$HOME/.cargo/env"

Windows

Download and run rustup-init.exe from https://rustup.rs. Rust on Windows needs the Microsoft C++ build tools — the installer will offer to install them, or grab the "Desktop development with C++" workload from the Visual Studio Installer. On Windows you can also install via a package manager:

winget install Rustlang.Rustup

Verify the install

In any terminal, check the versions:

rustc --version    # e.g. rustc 1.82.0
cargo --version    # e.g. cargo 1.82.0
Keeping Rust up to date is one command: rustup update. To uninstall everything cleanly: rustup self uninstall.

3. Cargo — Your First Project

Cargo creates projects, compiles them, runs them, downloads dependencies, and runs tests. You'll rarely call rustc directly. Create a new project:

cargo new hello
cd hello

That scaffolds a folder:

hello/
├─ Cargo.toml      # project name, version, dependencies
└─ src/
   └─ main.rs      # your code — starts with a hello-world main()

The generated src/main.rs already prints a greeting. Build and run it in one step:

cargo run
CommandWhat it does
cargo runCompile (if needed) and run the program
cargo buildCompile only — binary lands in target/debug/
cargo build --releaseOptimized build in target/release/ — much faster, slower to compile
cargo checkType-check without producing a binary — the fastest feedback loop
cargo testRun all tests in the project
cargo fmtAuto-format your code to the standard style
cargo clippyLint for common mistakes and non-idiomatic code
For tiny one-file experiments you don't even need Cargo — rustc main.rs then ./main works. But use Cargo for anything real; it's the whole ecosystem.

4. Program Structure

Every executable Rust program has a main function — it's where execution begins.

fn main() {
    println!("Hello, World!");
}
// Formatting with placeholders — {} is filled in order
let name = "Alice";
let age = 30;
println!("{} is {} years old.", name, age);

// Named / inline variables in the format string (Rust 2021+)
println!("{name} is {age} years old.");
Common macros you'll use constantly: println!, print!, format! (returns a String instead of printing), vec! (builds a vector), and panic! (crashes with a message).

5. Variables & Data Types

// Variables are IMMUTABLE by default
let x = 5;
// x = 6;  // ERROR — cannot assign twice to an immutable variable

// Add `mut` to make a variable changeable
let mut count = 0;
count = count + 1;

// Explicit type annotation (usually inferred, but you can be specific)
let price: f64 = 9.99;
let active: bool = true;

// Shadowing — reuse a name with `let`, even changing the type
let spaces = "   ";         // &str
let spaces = spaces.len();  // now usize — a new variable, same name

// Constants — always typed, ALL_CAPS, valid anywhere
const MAX_POINTS: u32 = 100_000;
TypeDescriptionExample
i8 i16 i32 i64 i128Signed integers (i32 is the default)-42
u8 u16 u32 u64 u128Unsigned integers42
usize / isizePointer-sized int — used for indexing0
f32, f64Floating point (f64 is the default)3.14
boolBooleantrue
charA single Unicode scalar (4 bytes)'A'
&str / StringBorrowed text / owned, growable text"hi"
let defines a variable. Immutable-by-default is a core Rust idea: you opt in to mutation with mut, so anything that can change is visible at a glance.

6. Operators

let (a, b) = (10, 3);
a + b    // 13
a - b    // 7
a * b    // 30
a / b    // 3  — integer division
a % b    // 1  — remainder

// Comparison
a == b   // false
a != b   // true
a < b    // false

// Logical
a > 5 && b < 5   // true  — AND
a > 5 || b > 5   // true  — OR
!(a == b)         // true  — NOT

// Compound assignment (needs `mut`)
let mut n = 5;
n += 1;   // 6   — also -=, *=, /=, %=
Rust has no ++ or -- operators. Use n += 1 and n -= 1 instead.

7. Strings

Rust has two main string types, and the difference trips up every beginner. String is an owned, growable, heap-allocated string. &str (a "string slice") is a borrowed view into text you don't own — string literals are &str.

// A literal is a &str
let greeting: &str = "Hello";

// Make an owned, growable String
let mut s = String::from("Hello");
s.push_str(", world");   // append a &str
s.push('!');            // append a single char

// Convert &str -> String
let owned = "hi".to_string();

// Concatenate with format! (returns a String)
let first = "Alice";
let last = "Smith";
let full = format!("{first} {last}");   // "Alice Smith"

// Useful methods
"hello".to_uppercase()          // "HELLO"
"  hi  ".trim()               // "hi"
"hello".contains("ell")       // true
"hello".starts_with("he")     // true
"hello".len()                  // 5  — length in BYTES
"a,b,c".split(',')             // an iterator over "a","b","c"

// Parse a string into a number — returns a Result
let n: i32 = "42".parse().unwrap();   // 42
Rule of thumb: take &str as function arguments (it accepts both literals and borrowed Strings), and return String when you're handing back owned text.

8. Arrays, Vectors, Tuples & Maps

Arrays — fixed size, known at compile time

let a: [i32; 3] = [10, 20, 30];
a[0]        // 10
a.len()     // 3
let zeros = [0; 5];  // [0, 0, 0, 0, 0]

Vectors — growable, use these in practice

let mut fruits = vec!["apple", "banana", "cherry"];

fruits[0]                    // "apple"
fruits.len()                 // 3
fruits.push("date");          // add to the end
fruits.pop();                 // remove & return the last (as Option)

// Safe access — get() returns an Option instead of panicking
match fruits.get(10) {
    Some(f) => println!("got {f}"),
    None    => println!("out of range"),
}

// An empty vector you push into later needs a type
let mut nums: Vec<i32> = Vec::new();
nums.push(1);

Tuples — fixed group of mixed types

let point = (3, 4, "origin");
point.0              // 3  — access by position

// Destructure a tuple into named variables
let (x, y, label) = point;

HashMap — key/value pairs

use std::collections::HashMap;

let mut ages = HashMap::new();
ages.insert("Alice", 30);
ages.insert("Bob", 25);

// Lookup returns an Option<&V>
if let Some(age) = ages.get("Alice") {
    println!("Alice is {age}");
}

9. Control Flow & match

if / else if / else

let score = 75;

if score >= 90 {
    println!("A");
} else if score >= 75 {
    println!("B");
} else {
    println!("C or below");
}

// if is an EXPRESSION — it can return a value
let grade = if score >= 60 { "pass" } else { "fail" };

match — the pattern-matching powerhouse

match is like a supercharged switch. It must be exhaustive — every possible case has to be handled, or the code won't compile. That's a feature: you can't forget a case.

let day = "Fri";

match day {
    "Mon"                => println!("Monday"),
    "Fri" | "Sat"         => println!("End of week"),
    _                    => println!("Other"),  // _ = catch-all
}

// match on number ranges, and return a value
let label = match score {
    0..=59   => "fail",
    60..=100 => "pass",
    _        => "invalid",
};

10. Loops

for — iterate over ranges and collections

// Range: 0, 1, 2, 3, 4  (upper bound excluded)
for i in 0..5 {
    println!("{i}");
}

// 0..=5 includes 5. Iterate a collection with .iter()
let fruits = vec!["apple", "banana"];
for fruit in &fruits {
    println!("{fruit}");
}

// Need the index too? .enumerate() gives (index, value)
for (i, fruit) in fruits.iter().enumerate() {
    println!("{i}: {fruit}");
}

while & loop

// while — runs while the condition is true
let mut n = 0;
while n < 5 {
    n += 1;
}

// loop — infinite until you `break`; can return a value
let mut i = 0;
let result = loop {
    i += 1;
    if i == 10 {
        break i * 2;   // break out AND hand back a value → 20
    }
};
Use break to exit a loop and continue to skip to the next iteration — same as most languages.

11. Functions

// Parameters are typed; -> declares the return type
fn greet(name: &str) -> String {
    format!("Hello, {name}!")   // no semicolon = this is the return value
}

// The last expression (no semicolon) is returned implicitly.
// You can also use an explicit `return` to leave early.
fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        return 0.0;
    }
    a / b
}

fn main() {
    println!("{}", greet("Alice"));
    println!("{}", divide(10.0, 3.0));
}

// Closures — anonymous inline functions that can capture variables
let double = |n: i32| n * 2;
double(5);   // 10
A key Rust habit: the final expression in a function body, written without a semicolon, is what the function returns. Adding a semicolon turns it into a statement that returns nothing (()).

12. Ownership & Borrowing

This is the idea that makes Rust Rust. It's how the language guarantees memory safety with no garbage collector. Three rules:

// MOVE: ownership of the String transfers from s1 to s2
let s1 = String::from("hello");
let s2 = s1;
// println!("{s1}");  // ERROR — s1 was moved, it's no longer valid
println!("{s2}");        // fine

To let another function use a value without taking ownership, you borrow it with & — a reference. The original owner keeps the value.

// &String is an immutable borrow — read, don't move
fn length(s: &String) -> usize {
    s.len()
}

let name = String::from("Alice");
let n = length(&name);   // borrow it
println!("{name} is {n} chars");   // name is still usable

// &mut is a mutable borrow — you can change what it points to
fn shout(s: &mut String) {
    s.push_str("!!!");
}
let mut msg = String::from("hi");
shout(&mut msg);   // msg is now "hi!!!"
The borrow rule: you may have either any number of immutable references (&) or exactly one mutable reference (&mut), but never both at once. That single rule is what eliminates data races at compile time.

13. Structs & Methods

// Define a struct
struct Person {
    name: String,
    age: u32,
}

// Attach methods with an impl block
impl Person {
    // An "associated function" — like a constructor. No self.
    fn new(name: &str, age: u32) -> Person {
        Person { name: name.to_string(), age }
    }

    // A method — takes &self to read the struct
    fn describe(&self) -> String {
        format!("{} is {} years old.", self.name, self.age)
    }

    // Takes &mut self to modify the struct
    fn birthday(&mut self) {
        self.age += 1;
    }
}

fn main() {
    let mut p = Person::new("Alice", 30);
    println!("{}", p.describe());   // Alice is 30 years old.
    p.birthday();
    println!("{}", p.age);          // 31
}
Add #[derive(Debug)] above a struct to get automatic debug printing, then println!("{p:?}", p = p) shows all its fields — invaluable while learning.

14. Enums, Option & Result

An enum is a type that is one of several named variants. Rust enums can also carry data, which makes them far more powerful than enums in most languages.

enum Shape {
    Circle(f64),          // carries a radius
    Rectangle(f64, f64), // carries width, height
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r)       => 3.14159 * r * r,
        Shape::Rectangle(w, h) => w * h,
    }
}

Two enums from the standard library are everywhere in Rust. Learn them early:

// Option — a value that might be absent. Rust has no `null`.
enum Option<T> { Some(T), None }

let maybe: Option<i32> = Some(5);
match maybe {
    Some(n) => println!("got {n}"),
    None    => println!("nothing"),
}

// Result — an operation that can succeed (Ok) or fail (Err)
enum Result<T, E> { Ok(T), Err(E) }
Because there is no null, "a value might be missing" is written into the type as Option<T>. The compiler forces you to handle the None case — that's how Rust abolishes null-pointer crashes.

15. Error Handling

Rust has no exceptions. Recoverable failures are returned as a Result; you decide how to handle them. The ? operator makes propagating errors clean.

use std::num::ParseIntError;

// Returns Ok(number) or Err(reason)
fn double_str(s: &str) -> Result<i32, ParseIntError> {
    let n: i32 = s.parse()?;   // ? = on Err, return it now
    Ok(n * 2)
}

fn main() {
    match double_str("21") {
        Ok(v)  => println!("doubled: {v}"),   // doubled: 42
        Err(e) => println!("bad input: {e}"),
    }
}

Two quick shortcuts for prototyping — but be deliberate about them:

Reach for match or ? in real code, and reserve .unwrap() for quick experiments and cases you've proven can't fail. A panic ends the program.

16. Traits & Generics

A trait defines shared behavior — a set of methods a type promises to provide. It's Rust's version of an interface.

trait Describable {
    fn describe(&self) -> String;
}

struct Dog { name: String }

impl Describable for Dog {
    fn describe(&self) -> String {
        format!("{} is a dog", self.name)
    }
}

// Generics — write a function once, use it for any type
// The bound <T: PartialOrd> means "any T that can be compared with <"
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list {
        if item > max {
            max = item;
        }
    }
    max
}

largest(&[3, 7, 2, 9, 4]);   // 9
Traits + generics are how Rust stays fast and reusable: the compiler generates a specialized copy of a generic function for each concrete type you use it with, so there's no runtime cost.
Ready to build something? Head to the Learning to Code by Bowling (Rust) build-along and put match, vectors, and loops to work on a real program.