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.
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.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 with cargo run:
fn main() {
println!("Hello, world!");
}
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.
| 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 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;, and curly braces { } define blocks and scope{} in the format string is a placeholder, filled in order; {name} pulls a variable in directly (Rust 2021+)fn main() {
let name = "Alice";
let age = 30;
println!("Hello, World!");
println!("{} is {} years old.", name, age);
println!("{name} is {age} years old.");
}
Hello, World! Alice is 30 years old. Alice is 30 years old.
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).// 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" |
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}");
}
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.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:
fn main() {
let used = 10;
let unused = 99; // declared but never read
println!("used = {used}");
}
cargo run printswarning: 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.
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.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
++ or -- operators. Use n += 1 and n -= 1 instead (the compound forms -=, *=, /=, %= all work too).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}");
}
sum=13, diff=7, prod=30 quotient=3, remainder=1 a > b is true n after += 1: 6
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
&str as function arguments (it accepts both literals and borrowed Strings), and return String when you're handing back owned text.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);
}
Hello, world! Alice Smith has 11 characters HELLO parsed then doubled: 84
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
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}");
}
}
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.matchif 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"),
}
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}");
}
grade: B End of week 75 is a pass
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() { /* ... */ }
break to exit a loop and continue to skip to the next iteration — same as most languages.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}");
}
sum of 1..=5 = 15 0: apple 1: banana doubled: 20
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
}
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));
}
Hello, Alice! 10 / 3 = 3.3333333333333335 double(5) = 10
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.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.
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}");
}
hello Alice is 5 chars hi!!!
&) 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.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.
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);
}
Alice is 30 years old. after birthday, age is 31
#[derive(Debug)] above a struct to get automatic debug printing, then println!("{p:?}") shows all its fields — invaluable while learning.Option & ResultAn 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).
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"),
}
}
area = 12.56636 area = 12 got 5
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: on an Err, it returns that error from the function immediately.
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}"),
}
}
doubled: 42 bad input: invalid digit found in string
.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.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 <."
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]));
}
Rex is a dog largest int: 9 largest float: 3.8
largest function worked on both integers and floats above.match, vectors, and loops to work on a real program.