Why I Finally Learned Rust
I've been curious about Rust for a while but never had a reason to start. Then a project needed a CLI tool with native performance and zero runtime dependencies. Rust fit perfectly.
Three months later, the borrow checker went from "impossible" to "actually helpful." Here's everything I wish I had on day one.
1. Install Rust on Ubuntu — The Right Way
Don't use apt install rustc. It's usually outdated. Use rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shFollow the prompts. Choose default toolchain. Restart your shell or run:
source ~/.cargo/envYou'll also need a C linker. On Ubuntu:
sudo apt install build-essentialVerify:
rustc --version # e.g. 1.96.0
cargo --version # e.g. 1.96.0To update later:
rustup update2. Your First Project
cargo new my_first_rust
cd my_first_rust
cargo runYou should see Hello, world! in under 5 seconds. That's your sandbox — mess with it freely.
3. VS Code Extensions You'll Actually Need
Two extensions, no more:
- rust-analyzer — Real-time compiler errors, autocomplete, inline hints, and built-in formatting (powered by
rustfmt, which ships with Rust) - CodeLLDB — Debugger (better than trying to debug Rust with print statements)
Enable format on save in VS Code settings:
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}4. The Commands I Use Every Day
Build & Run
cargo build # Compile (debug mode, fast)
cargo build --release # Compile (optimized, slow)
cargo run # Build + run in one shot
cargo run --bin <name> # Run a specific binary in a workspaceChecking Without Building
cargo check # Fastest — verify types/errors without generating binary
cargo clippy # Linter — catches common mistakes cargo check missesThis is the loop I use constantly: edit → cargo check → edit → cargo run.
Formatting & Testing
cargo fmt # Format all code (run before every commit)
cargo test # Run all tests
cargo test <name> # Run a specific test by name
cargo test -- --nocapture # See print statements in testsDocumentation
cargo doc --open # Build docs and open in browser
cargo doc --no-deps # Build docs for your crate onlyDependencies
cargo add <crate> # Add a dependency (e.g. cargo add serde)
cargo add <crate> --dev # Add as dev-dependency
cargo update # Update versions in Cargo.lock
cargo tree # Visualize dependency tree5. Understanding the Borrow Checker (Finally)
Here's the mental model that clicked for me:
Every value has exactly one owner. When the owner goes out of scope, the value is dropped.
let s1 = String::from("hello"); // s1 owns the string
let s2 = s1; // ownership MOVES to s2
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK: s2 is the ownerBorrowing lets you use a value without taking ownership:
fn length(s: &String) -> usize { s.len() }
let s = String::from("hello");
let len = length(&s); // Borrow s, don't take ownership
println!("{} still valid", s); // OK!The borrow checker enforces:
- Multiple immutable references (
&T) — OK, any number - OR one mutable reference (
&mut T) — but only one, and no immutable refs at the same time
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK — multiple immutable refs
// let r3 = &mut s; // ERROR: can't have mutable ref while immutable refs exist
println!("{} and {}", r1, r2);
// Now r1 and r2 are done using s, so:
let r3 = &mut s; // OK!
r3.push_str(", world");6. Common Patterns That Help
Pattern matching with match
let x = 3;
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("something else"),
}if let — less verbose than match
let Some(v) = maybe_value else { return; };
// vs
if let Some(v) = maybe_value {
println!("{}", v);
}Error handling with Result
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
match read_file("data.txt") {
Ok(content) => println!("File has {} bytes", content.len()),
Err(e) => eprintln!("Failed: {}", e),
}Or use the ? operator for early return on error:
fn get_config() -> Result<String, std::io::Error> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}7. Where to Go After the Basics
Once the compiler stops feeling like your enemy:
cargo install cargo-watch
cargo watch -x check -x test -x fmtThis runs check, test, and fmt automatically every time you save a file. Game changer.
Then start reading other people's code:
cargo search <keyword> # Find cratesAnd bookmark rustlings — interactive exercises that teach ownership by making you fix intentionally broken code.
The Bottom Line
Rust has a real learning curve. The borrow checker will reject your code hundreds of times before it starts making sense. But once it clicks, you'll write safer code in every language — because you'll understand why memory bugs happen.
Start with cargo new, run cargo check constantly, and don't try to understand lifetimes on day one. Save that for day thirty.
