# Learning Rust #1 — Tooling, Mindset, and a First Program
Table of Contents
I’ve shipped production systems in Python, PHP, and JavaScript. Rust’s been on my radar for years, but I never went all-in. This series changes that. I’ll document what actually matters when an experienced developer learns Rust—no fluff, no hero narratives, just sharp notes, working code, and the mental pivots you’ll feel along the way.
Goals for Part 1
- Install Rust (Windows/macOS/Linux) and verify the toolchain
- Set up VS Code with the right extensions
- Learn
cargobasics (new, build, run, test, fmt, clippy) - Understand ownership, borrowing, and moves at a practical level
- Build and run a tiny CLI, then refactor it with ownership in mind
Install & verify
Rust uses rustup to manage toolchains and components.
# macOS / Linuxcurl https://sh.rustup.rs -sSf | sh# follow the prompts, then:source $HOME/.cargo/env
# Windows# Download and run: https://win.rustup.rsVerify:
rustc --versioncargo --versionrustup --versionIf you’re on Windows and use WSL, install Rust inside WSL too (it’s a separate environment).
Toolchain hygiene (you’ll thank yourself later)
# keep things freshrustup update
# install rustfmt (formatter) and clippy (lints)rustup component add rustfmt clippyEditor setup (VS Code)
- Install Rust Analyzer (extension)
- Recommended settings:
{ "editor.formatOnSave": true, "rust-analyzer.check.command": "clippy", "rust-analyzer.inlayHints.enable": true}Cargo in 5 minutes
# create a new binary projectcargo new hello-rustcd hello-rust
# run (build + execute)cargo run
# format & lintcargo fmtcargo clippy -- -D warnings
# run tests (none yet, but get used to it)cargo testProject layout:
hello-rust/ ├─ Cargo.toml # manifest: deps, metadata └─ src/ └─ main.rs # entry pointFirst CLI: echo with count
use std::env;
fn main() { let args: Vec<String> = env::args().skip(1).collect();
if args.is_empty() { eprintln!("Usage: hello-rust <words...>"); std::process::exit(2); }
println!("You gave me {} words:", args.len()); for (i, w) in args.iter().enumerate() { println!("{:>3}: {}", i + 1, w); }}Run:
cargo run -- banana apple kiwiThe first big mental shift: Ownership
Rust’s memory model prevents whole classes of bugs at compile time. The rules are simple but strict:
- Every value has a single owner.
- When the owner goes out of scope, the value is dropped (freed).
- At any time, you may have either:
- one mutable reference
&mut T, or - any number of immutable references
&T.
- one mutable reference
Move vs Copy
Types like i32, bool, and small tuples are Copy. Moving them copies the bits. Heap-allocated types like String are moved (the pointer/len/cap struct is copied; the heap data isn’t).
fn main() { let x = 10; // Copy type let y = x; // copies bits println!("{x} {y}"); // both ok
let s1 = String::from("hello"); let s2 = s1; // move: s1 is invalidated // println!("{s1}"); // ❌ use after move println!("{s2}");}If you truly want to keep both, clone:
let s1 = String::from("hello");let s2 = s1.clone(); // allocates new bufferprintln!("{s1} {s2}");Rule of thumb: clone at the boundary (e.g., between threads or when caching), not deep inside loops.
Borrowing & References
Borrowing lets you refer to data without taking ownership.
fn len_of(s: &String) -> usize { s.len() } // borrow immutably
fn main() { let s = String::from("rust"); let n = len_of(&s); println!("{s} (len={n})"); // s still usable}Mutable borrowing:
fn shout(s: &mut String) { s.make_ascii_uppercase(); s.push('!');}
fn main() { let mut msg = String::from("hello"); shout(&mut msg); println!("{msg}");}Exclusivity is the superpower: at the moment you hold
&mut T, no one else can read or write thatT. Data races simply don’t compile.
Slices and &str vs String
String= owned, growable, heap-allocated UTF-8 buffer&str= borrowed view into UTF-8 bytes (often a slice of aStringor a string literal)
Prefer APIs that accept &str when you don’t need ownership:
fn greet(name: &str) -> String { format!("Hello, {name}!")}
fn main() { let owned = String::from("Pedro"); let lit = "Catarina"; println!("{}", greet(&owned)); println!("{}", greet(lit)); // &str coerce works}Typical beginner errors (and fixes)
“borrow of moved value”
let s = String::from("x");let t = s; // move// println!("{s}"); // ❌ use after moveFix: don’t use s after move, or .clone() intentionally, or borrow &s if you only need to read.
“cannot borrow as mutable because it is also borrowed as immutable”
let mut s = String::from("abc");let r1 = &s;let r2 = &s;// let r3 = &mut s; // ❌ while r1/r2 in scopeprintln!("{r1} {r2}");Fix: end the immutable borrows before taking &mut—use a smaller scope:
let mut s = String::from("abc");{ let r1 = &s; println!("{r1}");} // r1 ends herelet r3 = &mut s; // ✅r3.push('d');Refactor the CLI with borrowed views
Let’s adjust the first CLI to avoid unnecessary ownership where possible:
use std::env;
fn main() { // Keep owned args because env::args() yields owned Strings, // but we'll process them as &str views where we can. let args: Vec<String> = env::args().skip(1).collect(); if args.is_empty() { eprintln!("Usage: hello-rust <words...>"); std::process::exit(2); }
// Borrowed iteration: &String -> &str print_report(args.iter().map(|s| s.as_str()));}
fn print_report<'a, I>(words: I)where I: IntoIterator<Item = &'a str>,{ let collected: Vec<&str> = words.into_iter().collect(); println!("You gave me {} words:", collected.len()); for (i, w) in collected.iter().enumerate() { println!("{:>3}: {}", i + 1, w); }}The function takes an iterator of
&str—it’s flexible and avoids forcing callers to hand over ownership.
Quick quality loop
cargo fmtcargo clippy -- -D warningscargo testcargo run -- example one two threeAdd a tiny test:
#[cfg(test)]mod tests { #[test] fn sanity() { let x = 2 + 2; assert_eq!(x, 4); }}Debug skills you’ll actually use
dbg!macro prints value and file:line , returns the value (great in expressions).
let v = dbg!(vec![1, 2, 3]);RUST_BACKTRACE=1for panics with backtracescargo run --releasewhen you start micro-benching (it matters)
Cheatsheet (pin it)
- Prefer
&stroverStringin function parameters - Clone intentionally (at boundaries), not habitually
- End borrows by using tighter scopes
cargo fmtandcargo clippyearly & often- Read compiler errors slowly—they’re usually right and often suggest fixes
Exercises (10–20 minutes)
- Args filter: Extend the CLI to accept
--min-len Nand only list words of length ≥N. - Stable sort: Sort the words case-insensitively without creating a second owned
Vec<String>. (Hint:sort_by_keyon lowercase views.) - Borrow practice: Write a function
first_word(&str) -> &strthat returns the substring up to the first space (no allocations). - Ownership boundary: Write
owned_upper(s: &str) -> Stringthat returns an owned, uppercased copy. When is this preferable to in-place mutation?
What I learned today
- Ownership is a design tool, not a tax. It forces clean API boundaries.
- The compiler feels strict at first, then turns into a supportive reviewer.
- Rust’s build and lint tooling (
cargo,rustfmt,clippy) are delightfully integrated.
Next up (Part 2): deeper into the borrow checker & lifetimes—when explicit lifetimes appear, how to read them, and how to design APIs that make lifetimes disappear for callers.