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.
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.
.rs extensionThe 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.
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"
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
In any terminal, check the versions:
rustc --version # e.g. rustc 1.82.0
cargo --version # e.g. cargo 1.82.0
rustup update. To uninstall everything cleanly: rustup self uninstall.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
| Command | What it does |
|---|---|
cargo run | Compile (if needed) and run the program |
cargo build | Compile only — binary lands in target/debug/ |
cargo build --release | Optimized build in target/release/ — much faster, slower to compile |
cargo check | Type-check without producing a binary — the fastest feedback loop |
cargo test | Run all tests in the project |
cargo fmt | Auto-format your code to the standard style |
cargo clippy | Lint for common mistakes and non-idiomatic code |
rustc main.rs then ./main works. But use Cargo for anything real; it's the whole ecosystem.Every executable Rust program has a main function — it's where execution begins.
fn main() {
println!("Hello, World!");
}
fn declares a function; main is the entry pointprintln! ends in ! because it's a macro, not a function — macros are how Rust does variadic, format-checked printing;{ } define blocks and scope// 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.");
println!, print!, format! (returns a String instead of printing), vec! (builds a vector), and panic! (crashes with a message).// 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;
| Type | Description | Example |
|---|---|---|
i8 i16 i32 i64 i128 | Signed integers (i32 is the default) | -42 |
u8 u16 u32 u64 u128 | Unsigned integers | 42 |
usize / isize | Pointer-sized int — used for indexing | 0 |
f32, f64 | Floating point (f64 is the default) | 3.14 |
bool | Boolean | true |
char | A single Unicode scalar (4 bytes) | 'A' |
&str / String | Borrowed 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.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 -=, *=, /=, %=
++ or -- operators. Use n += 1 and n -= 1 instead.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
&str as function arguments (it accepts both literals and borrowed Strings), and return String when you're handing back owned text.let a: [i32; 3] = [10, 20, 30];
a[0] // 10
a.len() // 3
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
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);
let point = (3, 4, "origin");
point.0 // 3 — access by position
// Destructure a tuple into named variables
let (x, y, label) = point;
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}");
}
matchlet 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 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",
};
// 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 — 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
}
};
break to exit a loop and continue to skip to the next iteration — same as most languages.// 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
()).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!!!"
&) or exactly one mutable reference (&mut), but never both at once. That single rule is what eliminates data races at compile time.// 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
}
#[derive(Debug)] above a struct to get automatic debug printing, then println!("{p:?}", p = p) shows all its fields — invaluable while learning.Option & ResultAn 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) }
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.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:
.unwrap() — give me the value, or panic (crash) if it's Err/None.expect("message") — same, but crashes with your custom messagematch or ? in real code, and reserve .unwrap() for quick experiments and cases you've proven can't fail. A panic ends the program.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
match, vectors, and loops to work on a real program.