The systems language behind fast, memory-safe infrastructure — powering tools like ripgrep, the Deno runtime, parts of the Linux kernel, and increasingly cloud-native components. You'll learn Rust's defining idea (ownership), its fearless concurrency, error handling, and how to ship a small static binary. Practical and hands-on. Work top to bottom, or jump to a part.
The journey:
| You need | Why |
|---|---|
| Rust (via rustup) | Installs the compiler + cargo toolchain |
| A terminal + editor | cargo drives everything |
| Some programming experience | Rust's concepts land faster with a base |
Install with curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh, then confirm: rustc --version and cargo --version. Rust rewards patience — the compiler is strict but teaches you.
Rust's pitch: memory safety without a garbage collector, and data-race safety guaranteed at compile time. It achieves C/C++ performance while eliminating whole classes of bugs (use-after-free, null derefs, buffer overflows, data races) — not by a runtime, but by a compile-time ownership system.
For infrastructure that means: predictable low latency (no GC pauses), tiny static binaries, and confidence that concurrent code is sound. That's why it's showing up in performance-critical CLIs, network proxies, WebAssembly, and systems software.
Cargo is Rust's build tool, package manager, and test runner in one.
cargo new hello # scaffolds a project with git + a hello world
cd hello
cargo run # compile + run
// src/main.rs
fn main() {
let name = "Rust";
println!("Hello from {name}"); // ! means it's a macro
}
cargo new creates Cargo.toml (your manifest + dependencies) and src/main.rs. cargo run builds and runs; cargo build --release produces an optimized binary. Dependencies ("crates") come from crates.io.
This is what makes Rust different. Every value has a single owner; when the owner goes out of scope, the value is freed — no GC, no manual free. You borrow references instead of copying:
fn main() {
let s = String::from("config");
let len = length(&s); // borrow s immutably (&)
println!("{s} is {len} chars"); // s still usable — we only borrowed it
}
fn length(text: &String) -> usize {
text.len()
} // the borrow ends here; nothing is freed
The rules the compiler enforces: one owner at a time; you can have many immutable borrows (&) OR one mutable borrow (&mut), never both at once. This prevents data races and dangling pointers at compile time. Fighting the "borrow checker" is the Rust learning curve — but once it clicks, whole bug categories vanish.
Model data with structs and enums (far more powerful than in most languages), then destructure with match:
struct Server { name: String, port: u16 }
enum Health {
Up,
Degraded(String), // enums can carry data
Down,
}
fn describe(h: &Health) -> String {
match h { // exhaustive — must cover every case
Health::Up => "healthy".to_string(),
Health::Degraded(reason) => format!("degraded: {reason}"),
Health::Down => "down".to_string(),
}
}
match is exhaustive: the compiler forces you to handle every variant, so you can't forget a case. Enums-with-data model states precisely (this is how Option and Result work, next).
Rust has no null and no exceptions. Absence is Option<T> (Some/None); fallible operations return Result<T, E> (Ok/Err):
use std::fs;
fn read_config(path: &str) -> Result<String, std::io::Error> {
let contents = fs::read_to_string(path)?; // ? returns early on Err
Ok(contents)
}
fn main() {
match read_config("app.toml") {
Ok(text) => println!("{} bytes", text.len()),
Err(e) => eprintln!("failed: {e}"),
}
}
The ? operator propagates errors concisely — if the call is an Err, it returns it immediately. Because errors are values in the type signature, you literally cannot forget to handle them. Option does the same for "might be missing," eliminating null-pointer bugs.
Traits are Rust's interfaces; generics make code reusable with zero runtime cost:
trait Check {
fn check(&self) -> bool;
}
impl Check for Server {
fn check(&self) -> bool { self.port > 0 }
}
// Generic over anything that implements Check
fn all_healthy<T: Check>(items: &[T]) -> bool {
items.iter().all(|i| i.check())
}
Add dependencies with cargo and lean on the ecosystem: serde for JSON/YAML, tokio for async, clap for CLIs, reqwest for HTTP, anyhow/thiserror for ergonomic errors.
cargo add serde_json clap reqwest
The ownership rules that prevent memory bugs also prevent data races — the compiler rejects code that shares mutable state unsafely. For async network work, tokio is the standard runtime:
use std::thread;
fn main() {
let handles: Vec<_> = (0..4).map(|i| {
thread::spawn(move || { // move ownership into the thread
println!("worker {i}");
})
}).collect();
for h in handles { h.join().unwrap(); } // wait for all
}
Because the borrow checker enforces that shared data is either immutable or exclusively owned, concurrency bugs that plague other languages simply don't compile. For high-concurrency I/O, async/await on tokio scales to many thousands of tasks.
Rust binaries are self-contained and fast. Build an optimized release, then ship it in a minimal image:
cargo build --release # target/release/<name>, optimized
# ---- build ----
FROM rust:1.79 AS build
WORKDIR /src
COPY . .
RUN cargo build --release
# ---- runtime (tiny) ----
FROM gcr.io/distroless/cc
COPY --from=build /src/target/release/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
For a fully static binary (runs in scratch), target musl: rustup target add x86_64-unknown-linux-musl then cargo build --release --target x86_64-unknown-linux-musl. The result is a small, dependency-free container — like Go, but with even tighter control.
-D warnings).Result + ? everywhere; use anyhow for apps, thiserror for libraries.cargo clippy (the linter) and cargo fmt in CI — clippy catches idiom and correctness issues.cargo test — tests live alongside code; run them in CI.--release builds for anything shipped (debug builds are much slower).Cargo.lock (for binaries).unsafe and document why.clone() everything..unwrap() in production paths. It panics on Err/None. Handle with match/? and reserve unwrap for tests/prototypes.clone() to silence the compiler. Sometimes right, often a sign you should borrow — understand why first.--release; debug is far slower.unsafe early. You rarely need it; it opts out of the guarantees that make Rust worth it.cargo new, edit, cargo run, then cargo build --release and run the optimized binary.&str and returns a computed value; confirm the caller can still use the original.Health enum with a data-carrying variant and a match that handles every case.Result, use ? to propagate, and handle it with match in main.Self-check:
& OR one &mut).?/match over .unwrap() in real code?You now have the loop: cargo → ownership → enums/match → Result → traits → fearless concurrency → static binary. That's Rust for safe, fast infrastructure.