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.
Every numbered section below ends with a Try it box: a complete program you can paste into src/main.rs and run with cargo run, followed by the exact output it produces. Hover any code block and click Copy to grab it.

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 with cargo run:

Try it — the generated program
fn main() {
    println!("Hello, world!");
}
Output of cargo run
   Compiling hello v0.1.0 (/path/to/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/hello`
Hello, world!

The first three lines are Cargo reporting its work; the last line is your program's output. In every "Try it" box below, we show only your program's output — Cargo's build lines are trimmed for clarity.

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.

Try it — a complete program
fn main() {
    let name = "Alice";
    let age = 30;
    println!("Hello, World!");
    println!("{} is {} years old.", name, age);
    println!("{name} is {age} years old.");
}
Output
Hello, World!
Alice is 30 years old.
Alice is 30 years old.
Save the program as src/main.rs in your hello project and run cargo run. Other macros you'll use constantly: 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"
Try it — print variables & shadowing
fn main() {
    let name = "Alice";
    let age = 30;
    let mut count = 0;
    count += 1;
    println!("{name} is {age}, count is {count}");

    // Shadow `spaces`: it starts as text, becomes its length
    let spaces = "   ";
    let spaces = spaces.len();
    println!("spaces is now {spaces}");
}
Output
Alice is 30, count is 1
spaces is now 3
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.

Unused variables are a warning, not an error

If you declare a variable and never read it, Rust compiles and runs your program anyway — but it prints a friendly warning pointing right at the unused name. This is one of the first compiler messages every Rust beginner sees, so let's trigger it on purpose:

Try it — declare a variable you never use
fn main() {
    let used = 10;
    let unused = 99;   // declared but never read
    println!("used = {used}");
}
What cargo run prints
warning: unused variable: `unused`
 --> src/main.rs:3:9
  |
3 |     let unused = 99;
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused`
  |
  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: `hello` (bin "hello") generated 1 warning

used = 10

Read the message top to bottom: it's a warning (yellow, not a red error), it names the file and line (src/main.rs:3:9), it underlines the exact culprit, and it even suggests the fix — rename unused to _unused (a leading underscore tells Rust "I meant to leave this unused"). Crucially, the very last line is your program's real output: used = 10. The program still built and ran.

The difference that matters: a warning lets the program run; an error stops compilation entirely. Try changing println!("used = {used}") to println!("value = {unused_typo}") and you'll get a red error[E0425]: cannot find value instead — and no program output at all.

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
Rust has no ++ or -- operators. Use n += 1 and n -= 1 instead (the compound forms -=, *=, /=, %= all work too).
Try it — a complete program
fn main() {
    let (a, b) = (10, 3);
    println!("sum={}, diff={}, prod={}", a + b, a - b, a * b);
    println!("quotient={}, remainder={}", a / b, a % b);
    println!("a > b is {}", a > b);
    let mut n = 5;
    n += 1;
    println!("n after += 1: {n}");
}
Output
sum=13, diff=7, prod=30
quotient=3, remainder=1
a > b is true
n after += 1: 6

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.

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

// Useful methods
"hello".to_uppercase()          // "HELLO"
"  hi  ".trim()               // "hi"
"hello".contains("ell")       // true
"hello".len()                  // 5  — length in BYTES
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.
Try it — build, format & parse
fn main() {
    let mut s = String::from("Hello");
    s.push_str(", world");
    s.push('!');
    println!("{s}");

    let first = "Alice";
    let last = "Smith";
    let full = format!("{first} {last}");
    println!("{full} has {} characters", full.len());
    println!("{}", "hello".to_uppercase());

    // parse() turns text into a number
    let n: i32 = "42".parse().unwrap();
    println!("parsed then doubled: {}", n * 2);
}
Output
Hello, world!
Alice Smith has 11 characters
HELLO
parsed then doubled: 84

8. Arrays, Vectors, Tuples & Maps

Arrays are fixed-size; vectors (Vec) grow and are what you'll use in practice. Tuples group a fixed set of mixed types, and HashMap stores key/value pairs.

// Fixed-size array
let a: [i32; 3] = [10, 20, 30];

// Growable vector — use these in practice
let mut v = vec![1, 2, 3];
v.push(4);          // add to the end

// Tuple — mixed types, access by position
let point = (3, 4, "origin");
point.0              // 3
Try it — vectors, tuples & a map
use std::collections::HashMap;

fn main() {
    let mut fruits = vec!["apple", "banana", "cherry"];
    fruits.push("date");
    println!("fruits: {:?} (len {})", fruits, fruits.len());
    println!("first: {}", fruits[0]);

    // Destructure a tuple into named variables
    let point = (3, 4, "origin");
    let (x, y, label) = point;
    println!("x={x}, y={y}, label={label}");

    let mut ages = HashMap::new();
    ages.insert("Alice", 30);
    ages.insert("Bob", 25);
    if let Some(age) = ages.get("Alice") {
        println!("Alice is {age}");
    }
}
Output
fruits: ["apple", "banana", "cherry", "date"] (len 4)
first: apple
x=3, y=4, label=origin
Alice is 30
{:?} is the "debug" format — it prints structures like vectors as ["apple", "banana", ...]. Plain {} only works for types that define a human-facing display; {:?} works for almost everything while you're exploring.

9. Control Flow & match

if in Rust is an expression — it can return a value. match is like a supercharged switch that 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.

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

// match — the first arm that fits runs; _ is the catch-all
match day {
    "Fri" | "Sat" => println!("End of week"),
    _            => println!("Other"),
}
Try it — if-expression & two matches
fn main() {
    let score = 75;
    let grade = if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" };
    println!("grade: {grade}");

    let day = "Fri";
    match day {
        "Mon" => println!("Monday"),
        "Fri" | "Sat" => println!("End of week"),
        _ => println!("Other"),
    }

    // match can test number ranges and return a value
    let label = match score {
        0..=59 => "fail",
        60..=100 => "pass",
        _ => "invalid",
    };
    println!("{score} is a {label}");
}
Output
grade: B
End of week
75 is a pass

10. Loops

Rust's for walks over a range (0..5 excludes 5; 1..=5 includes it) or a collection. There's also while, and loop for "run until I break" — and loop can hand a value back out of the break.

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

// Index + value together with .enumerate()
for (i, item) in list.iter().enumerate() { /* ... */ }
Use break to exit a loop and continue to skip to the next iteration — same as most languages.
Try it — for, enumerate & loop
fn main() {
    let mut sum = 0;
    for i in 1..=5 {
        sum += i;
    }
    println!("sum of 1..=5 = {sum}");

    let fruits = vec!["apple", "banana"];
    for (i, fruit) in fruits.iter().enumerate() {
        println!("{i}: {fruit}");
    }

    // loop returns the value you break with
    let mut i = 0;
    let doubled = loop {
        i += 1;
        if i == 10 {
            break i * 2;
        }
    };
    println!("doubled: {doubled}");
}
Output
sum of 1..=5 = 15
0: apple
1: banana
doubled: 20

11. Functions

Parameters are typed; -> declares the return type. The last 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.

fn greet(name: &str) -> String {
    format!("Hello, {name}!")   // no semicolon = returned
}
Try it — functions & a closure
fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        return 0.0;   // early return
    }
    a / b
}

fn main() {
    println!("{}", greet("Alice"));
    println!("10 / 3 = {}", divide(10.0, 3.0));
    // A closure — an inline anonymous function
    let double = |n: i32| n * 2;
    println!("double(5) = {}", double(5));
}
Output
Hello, Alice!
10 / 3 = 3.3333333333333335
double(5) = 10
Notice 10 / 3 prints all those digits — that's an honest f64 (64-bit floating point). Use a format precision like {:.2} to round: println!("{:.2}", 10.0 / 3.0) prints 3.33.

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:

To let another function use a value without taking ownership, you borrow it with & (a reference). Add &mut to borrow it mutably.

Try it — move, borrow & mutable borrow
fn length(s: &String) -> usize {
    s.len()               // borrow: read, don't take
}

fn shout(s: &mut String) {
    s.push_str("!!!");    // mutable borrow: change it
}

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;             // ownership MOVES to s2; s1 is now invalid
    println!("{s2}");

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

    let mut msg = String::from("hi");
    shout(&mut msg);
    println!("{msg}");
}
Output
hello
Alice is 5 chars
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. Try adding println!("{s1}") after the move — the compiler stops you with error[E0382]: borrow of moved value.

13. Structs & Methods

A struct groups related data. You attach methods with an impl block: &self reads the struct, &mut self modifies it, and an associated function with no self (like new) acts as a constructor.

Try it — a struct with methods
struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: &str, age: u32) -> Person {
        Person { name: name.to_string(), age }
    }
    fn describe(&self) -> String {
        format!("{} is {} years old.", self.name, self.age)
    }
    fn birthday(&mut self) {
        self.age += 1;
    }
}

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

14. Enums, Option & Result

An enum is a type that is one of several named variants, and Rust enums can carry data — which makes them far more powerful than enums in most languages. Two enums from the standard library are everywhere: Option<T> (a value that might be absent — Rust has no null) and Result<T, E> (success or failure).

Try it — a data-carrying enum & Option
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,
    }
}

fn main() {
    let shapes = vec![Shape::Circle(2.0), Shape::Rectangle(3.0, 4.0)];
    for s in &shapes {
        println!("area = {}", area(s));
    }

    let maybe: Option<i32> = Some(5);
    match maybe {
        Some(n) => println!("got {n}"),
        None => println!("nothing"),
    }
}
Output
area = 12.56636
area = 12
got 5
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: on an Err, it returns that error from the function immediately.

Try it — Result, ? and both outcomes
use std::num::ParseIntError;

// Returns Ok(number) on success or Err(reason) on failure
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}"),
        Err(e) => println!("bad input: {e}"),
    }
    match double_str("oops") {
        Ok(v) => println!("doubled: {v}"),
        Err(e) => println!("bad input: {e}"),
    }
}
Output
doubled: 42
bad input: invalid digit found in string
Two quick shortcuts for prototyping: .unwrap() gives you the value or panics (crashes) on failure, and .expect("message") does the same with your own message. Reach for match or ? in real code, and reserve .unwrap() for experiments and cases you've proven can't fail.

16. Traits & Generics

A trait defines shared behavior — a set of methods a type promises to provide (Rust's version of an interface). Generics let you write a function once and use it for any type; the bound <T: PartialOrd> means "any T that can be compared with <."

Try it — a trait & a generic function
trait Describable {
    fn describe(&self) -> String;
}

struct Dog {
    name: String,
}

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

// Works for any T that can be compared and copied
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    let d = Dog { name: String::from("Rex") };
    println!("{}", d.describe());
    println!("largest int: {}", largest(&[3, 7, 2, 9, 4]));
    println!("largest float: {}", largest(&[1.5, 0.2, 3.8]));
}
Output
Rex is a dog
largest int: 9
largest float: 3.8
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. The same largest function worked on both integers and floats above.
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.