Rust has been Stack Overflow's "most loved" programming language for eight consecutive years — a streak so unusual it borders on statistical anomaly. Developers who use Rust love it. But Rust also has a reputation for being difficult to learn, with concepts like ownership, borrowing, and lifetimes that don't exist in any mainstream language.
This tutorial is going to change that. By the end, you'll understand the key ideas and have written your first substantive Rust program. We'll go slow where it matters and fast where it doesn't.
Why Rust? The Honest Answer
Rust was created at Mozilla to solve a specific, painful problem: writing systems software (browser engines, OS kernels, embedded firmware) that is both fast and memory-safe.
The traditional tradeoff looked like this:
- C/C++: Blazing fast, full control — but manual memory management means use-after-free, null pointer dereferences, buffer overflows, and data races are your problem. The NSA has literally issued advisories about this.
- Java/Go/Python: Memory-safe thanks to a garbage collector — but GC pauses, runtime overhead, and limited low-level control.
Rust's bet was: what if you could get C-level performance and memory safety, without a garbage collector? The trick is moving memory-safety guarantees to compile time, enforced by the borrow checker.
Installation
The fastest way to install Rust is via rustup, the official toolchain manager:
# On Linux / macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# On Windows — download rustup-init.exe from rustup.rs
# or use winget:
winget install Rustlang.Rustup
After installation, you'll have access to rustc (the compiler) and cargo (the build tool and package manager). Cargo does almost everything — you'll rarely need to invoke rustc directly.
Your First Program
Let's create a new project:
cargo new hello_rust
cd hello_rust
Cargo creates a src/main.rs with a Hello World. Let's look at it and then expand it:
fn main() {
// Variables are immutable by default
let name = "World";
let mut count = 0; // mut makes it mutable
while count < 3 {
println!("Hello, {}! Count: {}", name, count);
count += 1;
}
}
Run it with cargo run. Already you can see a few Rust conventions: immutability by default (let vs let mut), and println! is a macro (the ! tells you that).
The Big Idea: Ownership
Ownership is Rust's most distinctive feature. Three rules govern it:
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped (memory freed)
- There can only be one owner at a time
Let's see what this means in practice:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2 — s1 is no longer valid
// This would be a compile error:
// println!("{}", s1); // error: value borrowed after move
println!("{}", s2); // fine
}
When you assign s1 to s2, ownership transfers (moves). Rust's type system enforces this at compile time — no runtime checking needed. This is how Rust prevents use-after-free: the compiler simply won't let it happen.
Borrowing: Using Without Owning
Moving ownership everywhere would be incredibly restrictive. That's where references come in. A reference lets you use a value without taking ownership:
fn main() {
let s = String::from("hello world");
// Pass a reference — s is "borrowed" but not moved
let len = calculate_length(&s);
println!("'{}' has {} characters", s, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
// s goes out of scope here, but it doesn't drop the value
// because it's just a reference
}
The borrow checker enforces two rules on references:
- You can have any number of immutable references (
&T), OR - Exactly one mutable reference (
&mut T) - But never both at the same time
This prevents data races at compile time. Impressive.
Enums and Pattern Matching
Rust's enums are far more powerful than in most languages. Combined with pattern matching, they're a joy to use:
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle { base: f64, height: f64 }, // named fields
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
fn main() {
let shapes = vec![
Shape::Circle(3.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle { base: 6.0, height: 4.0 },
];
for shape in &shapes {
println!("Area: {:.2}", area(shape));
}
}
Error Handling the Rust Way
Rust has no exceptions. Instead, functions that can fail return Result<T, E>:
use std::num::ParseIntError;
fn double_number(s: &str) -> Result<i32, ParseIntError> {
let n = s.trim().parse::<i32>()?; // ? propagates errors
Ok(n * 2)
}
fn main() {
match double_number("42") {
Ok(n) => println!("Doubled: {}", n),
Err(e) => println!("Error: {}", e),
}
// The ? operator is syntactic sugar for:
// match result { Ok(v) => v, Err(e) => return Err(e.into()) }
}
? operator is Rust's way of making error propagation ergonomic. It evaluates a Result/Option, returning the Ok/Some value if success, or early-returning the Err/None if failure. Use it liberally.
Structs and Methods
struct Post {
title: String,
content: String,
views: u32,
}
impl Post {
// Constructor (by convention)
fn new(title: &str, content: &str) -> Self {
Post {
title: title.to_string(),
content: content.to_string(),
views: 0,
}
}
// Method that takes &self (immutable borrow)
fn summary(&self) -> String {
format!("{} ({} views)", self.title, self.views)
}
// Method that takes &mut self (mutable borrow)
fn record_view(&mut self) {
self.views += 1;
}
}
fn main() {
let mut post = Post::new("Hello Rust", "Rust is amazing!");
post.record_view();
post.record_view();
println!("{}", post.summary()); // Hello Rust (2 views)
}
Where to Go Next
You've touched the core ideas. Here's the canonical learning path:
- The Rust Book (free at doc.rust-lang.org/book) — The authoritative guide, surprisingly readable
- Rustlings — Small exercises that build intuition for the borrow checker
- Rust by Example — Annotated code samples for specific concepts
- crates.io — Explore the ecosystem.
tokiofor async,serdefor serialization,axumfor web servers
The borrow checker will frustrate you at first. That frustration is the compiler teaching you to write safer code. Lean into it. After a few weeks, you'll start instinctively writing code that the borrow checker accepts — and you'll realize that code is genuinely better.
"Fighting the borrow checker is a rite of passage. Once you stop fighting it and start working with it, Rust becomes genuinely fun — and you'll find yourself annoyed by how other languages let you shoot yourself in the foot."
Good luck. The crab emoji 🦀 awaits you on the other side.