# 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.

Github

Goals for Part 1

  • Install Rust (Windows/macOS/Linux) and verify the toolchain
  • Set up VS Code with the right extensions
  • Learn cargo basics (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.

Terminal window
# macOS / Linux
curl https://sh.rustup.rs -sSf | sh
# follow the prompts, then:
source $HOME/.cargo/env
# Windows
# Download and run: https://win.rustup.rs

Verify:

Terminal window
rustc --version
cargo --version
rustup --version

If you’re on Windows and use WSL, install Rust inside WSL too (it’s a separate environment).

Toolchain hygiene (you’ll thank yourself later)

Terminal window
# keep things fresh
rustup update
# install rustfmt (formatter) and clippy (lints)
rustup component add rustfmt clippy

Editor setup (VS Code)

  • Install Rust Analyzer (extension)
  • Recommended settings:
.vscode/settings.json
{
"editor.formatOnSave": true,
"rust-analyzer.check.command": "clippy",
"rust-analyzer.inlayHints.enable": true
}

Cargo in 5 minutes

Terminal window
# create a new binary project
cargo new hello-rust
cd hello-rust
# run (build + execute)
cargo run
# format & lint
cargo fmt
cargo clippy -- -D warnings
# run tests (none yet, but get used to it)
cargo test

Project layout:

hello-rust/
├─ Cargo.toml # manifest: deps, metadata
└─ src/
└─ main.rs # entry point

First CLI: echo with count

src/main.rs
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:

Terminal window
cargo run -- banana apple kiwi

The first big mental shift: Ownership

Rust’s memory model prevents whole classes of bugs at compile time. The rules are simple but strict:

  1. Every value has a single owner.
  2. When the owner goes out of scope, the value is dropped (freed).
  3. At any time, you may have either:
    • one mutable reference &mut T, or
    • any number of immutable references &T.

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 buffer
println!("{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 that T. 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 a String or 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 move

Fix: 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 scope
println!("{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 here
let 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

Terminal window
cargo fmt
cargo clippy -- -D warnings
cargo test
cargo run -- example one two three

Add 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=1 for panics with backtraces
  • cargo run --release when you start micro-benching (it matters)

Cheatsheet (pin it)

  • Prefer &str over String in function parameters
  • Clone intentionally (at boundaries), not habitually
  • End borrows by using tighter scopes
  • cargo fmt and cargo clippy early & often
  • Read compiler errors slowly—they’re usually right and often suggest fixes

Exercises (10–20 minutes)

  1. Args filter: Extend the CLI to accept --min-len N and only list words of length ≥ N.
  2. Stable sort: Sort the words case-insensitively without creating a second owned Vec<String>. (Hint: sort_by_key on lowercase views.)
  3. Borrow practice: Write a function first_word(&str) -> &str that returns the substring up to the first space (no allocations).
  4. Ownership boundary: Write owned_upper(s: &str) -> String that 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.

Next: Learning Rust #2 — Making Friends with the Borrow Checker
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


Learning Rust Series

Comments