Rust Fundamentals
A Complete 8-Chapter Programming Course
Table of Contents
- Getting Started: cargo, rustc, and Your First Program
- Variables & Basic Types
- Ownership
- Borrowing & References
- Structs & Methods
- Enums & Pattern Matching
- Error Handling
- Collections & Iterators
Getting Started: cargo, rustc, and Your First Program
Rust is a compiled, systems-level language, in the same broad family as Go โ source code is built into a standalone executable, not interpreted line by line. But the two languages exist for genuinely different reasons, and that difference shapes everything in this course.
Why Rust Exists
Go, JavaScript, Python, Ruby, PHP, and Kotlin all rely on a garbage collector โ a background process that automatically frees memory the program no longer needs. This is convenient, but it comes with real costs: runtime overhead, and pause times that are hard to predict exactly when they'll happen. Older systems languages like C and C++ skip the garbage collector entirely, giving the programmer full manual control โ but with it, real risk: dangling pointers, buffer overflows, use-after-free bugs that have caused decades of security vulnerabilities.
Rust's entire reason for existing is refusing to accept that trade-off: manual-control-level performance and predictability, without the memory-safety bugs manual control usually invites โ enforced not at runtime, but by the compiler itself, before the program ever runs. Chapters 3โ4 cover the actual mechanism (ownership and borrowing); this chapter is just naming the goal everything else in this course serves.
Installing Rust: rustup and rustc
rustup is Rust's official installer and toolchain manager โ it installs and manages Rust versions, similar in spirit to a version manager, but built and maintained by the Rust team itself as the standard, recommended way to get Rust at all.
rustc is the raw compiler underneath everything โ the direct equivalent of what runs behind the scenes when Go's go build compiles a file. In practice, most day-to-day work goes through cargo instead of calling rustc directly.
cargo: Rust's Build Tool & Package Manager
This is the first real toolchain difference from Go. Go's toolchain is deliberately minimal โ go run and go build are close to the whole story. cargo bundles far more into one official tool from day one: building, running, dependency management against crates.io (Rust's package registry), and testing โ closer in spirit to npm and a bundler combined, except built directly into Rust's own official tooling rather than assembled from separate third-party choices.
Cargo.toml is the direct equivalent of Go's go.mod โ but where a go.mod stays minimal, Cargo.toml is the central place dependencies, metadata, and build configuration all live together.
The Smallest Rust Program
fn main() is the entry point โ the same role func main() plays in Go; execution always starts here. println! is Rust's equivalent of fmt.Println, but notice the ! โ that marks it as a macro, not an ordinary function call. Macros are a genuinely different Rust concept from anything in Go or JavaScript (Course 3's own Macros chapter covers writing them); for now, just recognize that ! after a name means "this is a macro," and println! is the one used constantly throughout this course.
| Concept | Go | Rust |
|---|---|---|
| Compile & run in one step | go run file.go | cargo run |
| Produce a standalone binary | go build | cargo build --release |
| Project manifest | go.mod (minimal) | Cargo.toml (build config + dependencies) |
| Print a line | fmt.Println(...) | println!(...) |
| Entry point | func main() | fn main() |
;) from an expression (no semicolon, and its value can be returned). Inside main right now this mostly just means "don't forget the semicolon" โ but this exact distinction becomes meaningful later when a function's last line, with no semicolon, becomes its return value. Missing or extra semicolons produce real compiler errors, not silent behavior differences.
Coding Challenges
Use cargo new to create a project called greeting, then edit src/main.rs so it prints your name and a short greeting using two separate println! calls. Run it with cargo run.
๐ View solutionWrite a program that prints "Year: 2026" and "Language: Rust" using println! with placeholders ({}) instead of concatenating strings โ two separate println! calls, each using a placeholder.
๐ View solutionExplain, in your own words, why Rust's pitch ("manual-control performance without manual-control memory bugs") is a genuinely different trade-off than either a garbage-collected language or C/C++ โ and name the two chapters coming up that actually deliver on that promise.
๐ View solutionChapter 1 Quick Reference
- Rust's core goal: memory safety without a garbage collector, enforced at compile time
- rustup โ the official installer/toolchain manager; rustc โ the raw compiler underneath
- cargo โ Rust's official build tool + package manager + test runner, bundled together (unlike Go's minimal toolchain)
- Cargo.toml โ the project manifest (dependencies, build config) โ Rust's fuller-featured counterpart to Go's
go.mod - cargo new / cargo run / cargo build --release โ create, run, and produce a standalone binary
- fn main() { } โ the entry point, same role as Go's
func main() - println!(...) โ note the
!: this is a macro, not an ordinary function - Semicolons distinguish statements from expressions โ a distinction that becomes load-bearing later, not just style
- Next chapter: variables, basic types, immutability by default (
letvslet mut), and shadowing
Variables & Basic Types
Chapter 1 named memory safety as Rust's whole reason for existing. This chapter's first rule is a direct expression of that philosophy: variables are immutable by default โ the opposite of what Go, JavaScript, and Python all do.
Immutability by Default
let x = 5; creates an immutable binding โ attempting to reassign it is a compile error, not something that fails at runtime or silently succeeds. Mutability has to be opted into explicitly:
This is a deliberate design choice, not an arbitrary restriction: most variables, in practice, are never reassigned after being set โ making immutability the default means the compiler can catch an accidental reassignment as a mistake, rather than a program silently doing something the programmer didn't intend.
Scalar Types
Rust's basic scalar types are explicit about size and signedness, unlike Go's simpler int/uint or JavaScript's single Number type:
Compound Types
A tuple groups a fixed number of values of mixed types; an array holds a fixed number of values of the same type. Both have a size fixed at compile time โ a dynamically growable list is Vec<T>, covered in Chapter 8.
Type Inference & Explicit Annotations
Rust infers types in most cases โ let x = 5; is inferred as i32 without an annotation, the same convenience Go's := provides. An explicit annotation is only needed when the inferred default isn't what's wanted, or when the type genuinely can't be inferred from context alone.
Shadowing
Rust allows re-declaring a variable with the same name using let again โ this creates a brand-new binding that shadows the previous one, and can even change its type entirely. This is genuinely different from Go, where redeclaring a name in the same scope with := is usually a compile error unless at least one new variable is being introduced.
The second let input doesn't mutate the original string variable โ it creates a separate variable that happens to share the name, and the earlier &str version is no longer accessible from this point in the scope onward.
| Concept | Go | Rust |
|---|---|---|
| Default mutability | Mutable by default | Immutable by default (mut opts in) |
| Type inference | := infers the type | let infers the type |
| Redeclaring the same name | Compile error (with :=, unless a new var is added) | Allowed โ shadowing, can change type |
| Fixed-size list | Array (rarely used directly) | [T; N] โ fixed-size, same type |
let again each time โ a plain reassignment (input = ... with no let) is mutation, and requires mut, an entirely different mechanism. Shadowing is also scope-limited: once a shadowed variable's block ends, the outer variable (if one exists) becomes visible again, unaffected by whatever the inner shadow did. Confusing shadowing with mutation is a common early mix-up โ they look superficially similar but follow completely different rules.
Coding Challenges
Declare an immutable variable holding your age as a u8, then write a second line that attempts to reassign it. Run cargo run, note the exact compiler error, then fix it using mut.
๐ View solutionCreate a tuple holding a product's id (i32), price (f64), and category initial (char). Print each field individually using tuple indexing (.0, .1, .2).
๐ View solutionUsing shadowing, take a variable holding the string "100", and shadow it twice: first into an i32, then into that number multiplied by 2. Print the final value, and explain why this required let each time rather than plain reassignment.
๐ View solutionChapter 2 Quick Reference
- let x = ... โ immutable by default; reassigning without
mutis a compile error - let mut x = ... โ explicitly opts into mutability
- Scalar types: i32/u32/i64/u64 (explicit width + signedness), f32/f64, bool, char (4-byte Unicode scalar)
- Tuples โ fixed size, mixed types, indexed with
.0/.1/.2 - Arrays โ fixed size, same type,
[T; N]; a growable list isVec<T>(Ch.8) - Shadowing โ re-declaring with
letcreates a new binding, can change type, scope-limited - Shadowing โ mutation โ shadowing needs
leteach time; mutation needsmut, no newlet - Next chapter: Ownership โ move semantics, one owner at a time, and why this eliminates the need for a garbage collector
Ownership
Chapter 1 promised memory safety without a garbage collector. This chapter is where that promise becomes concrete โ ownership is Rust's single defining idea, and nearly everything else in this course builds on it.
The Three Ownership Rules
- Each value has exactly one owner at a time.
- When the owner goes out of scope, the value is dropped (its memory freed) automatically.
- Ownership can be transferred (moved), but a value is never implicitly duplicated.
Every example in this chapter is really just these three rules playing out in different situations.
Stack vs. Heap, Briefly
Simple fixed-size types (i32, bool, char, tuples of these) live entirely on the stack โ cheap to copy, since copying just means duplicating a few fixed bytes. A String (or Vec, covered in Chapter 8) stores its actual character data on the heap, with only a pointer, length, and capacity sitting on the stack. This distinction is exactly why move semantics matter: moving a String is cheap โ it's just copying that small pointer/length/capacity trio, never the underlying heap data itself.
Move Semantics
Coming from Go or JavaScript, let s2 = s1 looks like it should leave both variables usable โ either both reference the same underlying string, or the whole value gets copied. Rust does neither: it moves ownership of the heap data from s1 to s2, and s1 becomes invalid from that point on. Using it afterward is a compile error, not a runtime surprise.
Why Not Just Copy Everything?
The alternative โ deep-copying on every assignment โ would be safe, but expensive, and that cost would be invisible in the code. Rust instead makes moving the default (cheap, just a pointer/length/capacity copy) and requires an explicit .clone() call whenever a real, expensive deep copy is actually wanted โ making that cost visible right in the source.
Simple stack-only types (i32, f64, bool, char, and tuples made entirely of these) implement Rust's Copy trait instead โ copying them is so cheap that Rust just does it automatically on assignment, and neither variable becomes invalid:
One Owner at a Time & Automatic Cleanup
When a variable goes out of scope โ the end of a block or function โ Rust automatically calls drop on it, freeing its memory immediately and deterministically. This is the actual mechanism that eliminates the need for a garbage collector: because ownership rules guarantee, at compile time, that exactly one thing is ever responsible for a given piece of data, the compiler always knows the exact moment cleanup should happen โ no background process needs to figure it out at runtime.
Ownership and Functions
Passing a value to a function moves it in (unless the type is Copy) โ the function becomes the new owner, and the caller's variable becomes invalid after the call, unless the function explicitly hands ownership back by returning the value.
This is genuinely inconvenient if the caller just wanted the function to look at the value without giving it up entirely โ which is exactly the problem Chapter 4's borrowing exists to solve.
| Move Types | Copy Types | |
|---|---|---|
| Examples | String, Vec<T>, most custom structs | i32, f64, bool, char, tuples of these |
| On assignment | Ownership moves; original becomes invalid | Value is copied; both remain valid |
| To get a real duplicate | Explicit .clone() | Automatic โ no clone needed |
& and &mut) is specifically designed to solve the exact frustration this chapter's function example just demonstrated โ letting a function use a value without taking ownership of it at all.
Coding Challenges
Write code that creates a String, moves it into a second variable, then attempts to print the original variable. Run cargo run, note the exact compiler error, then fix it using .clone() instead so both variables remain usable.
๐ View solutionExplain why let x = 5; let y = x; println!("{} {}", x, y); compiles fine with no error, while the equivalent pattern with a String does not โ referencing the Copy trait specifically.
๐ View solutionWrite a function that takes a String, prints it, and returns it back to the caller so the caller's variable stays usable after the call โ without using .clone() anywhere.
๐ View solutionChapter 3 Quick Reference
- Three rules: one owner at a time; drop happens automatically at scope end; a value is never implicitly duplicated
- Move โ assigning/passing a heap-backed value (String, Vec) transfers ownership; the original variable becomes invalid
- .clone() โ an explicit, real deep copy; makes the cost of copying visible in the code
- Copy trait โ simple stack-only types (i32, f64, bool, char) copy automatically on assignment; neither variable is invalidated
- drop โ called automatically when a variable's owner goes out of scope; deterministic, no garbage collector involved
- Passing a value to a function moves it in, unless the type is
Copyor the function returns it back - "Borrow of moved value" errors are normal and common โ not a sign of doing something wrong
- Next chapter: Borrowing & References โ
&and&mut, letting a function use a value without taking ownership
Borrowing & References
Chapter 3 ended with a real frustration: passing a value to a function moves it, leaving the caller unable to use it afterward. Borrowing is the fix โ letting a function use a value without taking ownership of it at all.
References With &
&s1 creates a reference to s1 rather than moving it โ calculate_length can read the string, but never owns it. Once the function returns, s1 is exactly as valid as before, solving Chapter 3's function-ownership problem directly.
Mutable References With &mut
&mut lets a function genuinely modify the borrowed value โ but this power comes with a strict rule.
The Borrow Checker's Rules
- At any given time, you can have either any number of immutable references (
&) or exactly one mutable reference (&mut) โ never both at once. - References must always be valid โ no reference to something that's already been dropped.
Both rules are enforced entirely at compile time.
| Situation | Allowed? |
|---|---|
Two immutable references (&s, &s) at once | Yes |
One mutable reference (&mut s) alone | Yes |
| One mutable + one immutable reference, both active at once | No โ compile error |
| Two mutable references, both active at once | No โ compile error |
Why These Rules Exist
The mutable-XOR-immutable rule prevents a real, classic bug class: something reading data while something else modifies it underneath it. Concretely โ if two mutable references to a growing collection existed simultaneously, one holder could trigger a reallocation (the collection outgrowing its current memory and moving elsewhere) while the other still points at the old, now-freed memory location โ a genuine dangling-pointer bug that has caused real, exploitable vulnerabilities in C++. Rust's borrow checker makes this class of bug simply not compile, rather than something to carefully avoid at runtime. This is also the foundation of Rust's "fearless concurrency" (Course 2) โ the exact same rule that prevents this single-threaded aliasing bug also prevents data races between threads.
Dangling References
In C or C++, this same pattern compiles and produces a genuine dangling pointer โ undefined behavior waiting to happen at runtime, possibly much later and far from where the actual mistake was made. In Rust, it's simply a compile error: the borrow checker sees that s is dropped at the end of the function, and refuses to let a reference to it escape.
Rust's Alternative to Go's GC
The full arc from Chapter 1 is now complete. Go's garbage collector scans memory at runtime to find and free data nothing references anymore โ genuinely useful, but with real runtime overhead and pause times that are hard to predict exactly when they'll land. Rust's borrow checker instead analyzes ownership and borrowing entirely at compile time โ by the time a Rust program actually runs, every memory-safety guarantee has already been proven, with zero runtime cost for that safety, no background process ever running, and the deterministic drop timing Chapter 3 introduced.
| Rule | What It Prevents |
|---|---|
| One owner at a time (Ch.3) | Ambiguity about who's responsible for freeing memory |
| Mutable XOR immutable references | Reading data while it's being modified (aliasing bugs, data races) |
| References must stay valid | Dangling references / use-after-free |
Coding Challenges
Write a function count_words that takes a &String and returns the number of words (split_whitespace().count()) without taking ownership. Call it twice on the same variable to prove it remains valid after each call.
๐ View solutionWrite code that creates one mutable String, then attempts to hold both an immutable reference and a mutable reference to it at the same time (using both before the function ends). Run cargo run, note the exact compiler error, then fix it by ensuring the immutable reference's last use happens before the mutable one is created.
๐ View solutionExplain, in your own words, why Rust's approach to memory safety has zero runtime cost compared to Go's garbage collector โ referencing specifically when each language's safety mechanism actually does its work.
๐ View solutionChapter 4 Quick Reference
- &value โ an immutable reference; the function can read but not modify, and never takes ownership
- &mut value โ a mutable reference; allows modification, but only one may exist at a time
- Borrow rule: any number of
&references, OR exactly one&mutreference โ never both simultaneously - References must stay valid โ the compiler rejects any reference that could outlive the data it points to
- These rules prevent aliasing bugs and data races at compile time โ the same foundation behind Rust's "fearless concurrency"
- Rust vs. Go's GC: Rust proves memory safety at compile time (zero runtime cost); Go's GC checks at runtime (real, ongoing overhead)
- Non-lexical lifetimes: a reference's effective scope ends at its last use, not the closing brace โ full lifetime syntax is Course 2's territory
- Next chapter: Structs & Methods โ struct definitions, impl blocks, and methods vs. associated functions
Structs & Methods
With ownership and borrowing established, this chapter combines fields into custom types and gives them behavior โ territory Go's own go2-1 (Structs, Methods, Pointers) already covered, making this a natural place for direct side-by-side comparison.
Defining a Struct
A named collection of typed fields, accessed with dot notation โ the same basic shape as a Go struct definition.
impl Blocks
Rust keeps a struct's data definition and its behavior definition separate โ fields live in struct, methods live in a separate impl ("implementation") block. This is actually a genuine similarity to Go, not a contrast: Go also separates data (the struct) from behavior (a function with a receiver), unlike Java or Kotlin, which bundle both inside one class body.
Methods (&self)
A method's first parameter is &self โ a borrowed reference to the instance (Chapter 4's & in action), giving read access to its fields without taking ownership. Called with dot syntax: rect.area().
| Concept | Go | Rust |
|---|---|---|
| Where methods are defined | Outside the struct, with a receiver | Inside a separate impl block |
| Receiver syntax | func (r Rectangle) Area() float64 | fn area(&self) -> f64 |
| Calling a method | rect.Area() | rect.area() |
Associated Functions (No self)
A function inside an impl block without self is an associated function โ not a method, and it can't be called on an instance with dot syntax. It's called via Struct::function_name() instead. The most common use is a constructor:
This is exactly the same :: syntax that's already appeared constantly since Chapter 1 โ String::from("hello") is nothing more than a call to an associated function named from defined in String's own impl block. Go has no real language equivalent: a Go "constructor" is just an ordinary function following a naming convention (NewRectangle(width, height float64) Rectangle) โ Rust's :: namespacing is a genuine language feature tied directly to the type itself, not a convention.
String::from(...) call since Chapter 1 has been calling String's own from associated function โ the exact pattern this chapter's Rectangle::new(...) example just defined from scratch. Nothing about it was ever special syntax; it's just a constructor-style associated function, precisely like the ones you can now write yourself.
&self (borrow, read-only), &mut self (borrow, mutable), or plain self (by value) โ and that last form genuinely moves the instance into the method, per Chapter 3's move semantics. After calling a method that takes self by value, the original variable is no longer usable at all, exactly like passing it into any other function. Coming from a language where "this"/"self" is always just an implicit reference with no ownership implications, this is an easy trap โ reach for &self by default, and use plain self only when a method is deliberately meant to consume the instance (e.g. transforming it into something else).
Coding Challenges
Define a struct Circle with a single field radius (f64). Write a method area(&self) that returns the circle's area (use 3.14159 for pi), and call it on an instance.
๐ View solutionAdd an associated function Circle::new(radius: f64) -> Circle to Challenge 1's struct, and use it to construct an instance instead of the struct-literal syntax.
๐ View solutionExplain the difference between a method and an associated function in Rust, and why Struct::new(...) can't be called as instance.new(...) the way area() can be called as instance.area().
๐ View solutionChapter 5 Quick Reference
- struct Name { field: Type, ... } โ defines a custom data type
- impl Name { } โ a separate block holding methods and associated functions for that struct
- Method: first parameter is
&self/&mut self/self; called withinstance.method() - Associated function: no
selfparameter; called withStruct::function()โ the pattern behind everyString::from(...)call so far selfby value moves/consumes the instance โ it's unusable afterward, per Chapter 3's move rules- Rust and Go both separate data (struct) from behavior โ unlike Java/Kotlin's class-bundled approach
- Rust's
::constructor pattern is a real language feature; Go'sNewX()is only a naming convention - Next chapter: Enums & Pattern Matching โ enums that carry data, the
matchexpression, andOption<T>replacing null entirely
Enums & Pattern Matching
Chapter 5 covered structs. This chapter covers Rust's other major custom type โ and it's genuinely more powerful than what Go calls an "enum," which is really just a sequence of typed integer constants built with iota.
Enums That Carry Data
A Rust enum variant can hold its own associated data โ and different variants can hold entirely different shapes of data. Go's iota-based constants are just named integers; there's no way to attach data to one at all.
V4 and V6 aren't just labels โ each is effectively its own little data structure, holding whatever shape of data actually fits that variant.
The match Expression
Rust's match is a switch-like construct, but stricter and more useful: it's an expression (it produces a value), and it must be exhaustive โ every possible variant needs a matching arm, or an explicit _ catch-all. Missing a case is a compile error, not silent fall-through the way an incomplete switch in Go or JavaScript can behave.
Each arm destructures the variant's data directly โ IpAddr::V4(a, b, c, d) pulls all four numbers out in one line, ready to use immediately.
Option<T>: No Null At All
Rust has no null or nil value at all โ the concept doesn't exist in the language. Anything that might be absent is instead wrapped in the built-in Option<T> enum:
You cannot accidentally treat a None as if it were a real value โ the compiler forces every Option<T> to be handled (via match, and later if let/unwrap) before the real value inside a Some can be used at all.
| Go | Rust | |
|---|---|---|
| "Enum" | iota โ just typed integer constants | Variants can each carry their own data |
| Missing value | nil / zero value | Option<T> โ Some(T) or None |
| What happens if mishandled | Nil pointer dereference โ a runtime panic | Compile error โ must handle None before compiling |
Option<T> is Rust's structural answer: since null doesn't exist as a concept at all, an entire category of bugs โ null pointer/reference exceptions โ simply cannot occur, caught instead as a compile-time requirement to handle absence explicitly.
match elsewhere in a codebase fail to compile, if none of them have a catch-all _ arm. This can feel like a nuisance at first โ but it's the compiler doing exactly its job: pointing at every single place in the code that needs to be updated to actually handle the new case, rather than letting it silently fall through unhandled somewhere far from where the variant was added.
Coding Challenges
Define an enum Shape with variants Circle(f64) (radius) and Rectangle(f64, f64) (width, height). Write a match expression that computes the area for either variant.
๐ View solutionWrite a function find_first_even(numbers: &[i32]) -> Option<i32> that returns the first even number in a slice, or None if there isn't one. Call it with a match that prints either the found number or a "no even number" message.
Explain why Rust's Option<T> prevents a whole category of bugs that Go's nil pointers don't โ specifically, at what point each language catches the mistake of using an absent value.
Chapter 6 Quick Reference
- enum variants can carry their own data โ unlike Go's iota, which produces plain integer constants
- match is exhaustive โ every variant needs an arm, or an explicit
_catch-all, enforced at compile time - match arms can destructure a variant's data directly, ready to use in the arm's body
- Option<T> โ Rust's built-in replacement for null:
Some(T)orNone - The compiler forces every
Option<T>to be handled before the real value can be used โ no null pointer exceptions possible - Go's nil pointer dereference is a runtime panic; Rust's equivalent mistake is a compile-time error
- A match that breaks after adding a new enum variant is the compiler correctly flagging every spot that needs updating
- Next chapter: Error Handling โ
Result<T, E>, the?operator, andpanic!vs. recoverable errors
Error Handling
Chapter 6's Option<T> handled absence. This chapter handles the other "might not work" case: an operation that can fail with a reason why โ and, genuinely more than most mainstream languages, Rust's approach here has real common ground with Go's own explicit error handling.
Result<T, E>
Same basic shape as Option, but the failure case carries a value (E) explaining what went wrong, not just an absence.
Rust making errors part of the function's return type, rather than an exception, is genuinely the same core philosophy Go's own (value, error) convention already uses โ a real similarity, not just a contrast, distinguishing both languages from JavaScript/Python's try/catch exception model.
panic! vs. Recoverable Errors
panic! immediately stops the program โ reserved for genuinely unrecoverable situations: a real bug, a broken invariant, something that should structurally never happen. Result is for expected, recoverable failure conditions: a missing file, invalid user input, a failed network call. Rust's convention is clear about which to reach for: Result for anything a caller might reasonably want to handle; panic! only for "this should never happen."
The ? Operator
Go's error handling requires an explicit check after every single call that can fail: if err != nil { return err }. Rust's ? operator does the exact same propagation in one character: if the expression is Err, it returns that Err immediately from the current function; if it's Ok, it unwraps the value and execution continues.
| Go | Rust | |
|---|---|---|
| Error type | A separate error return value | Err(E) variant of Result<T, E> |
| Propagating an error up | if err != nil { return err } โ repeated at every call | ? โ one character, same call site |
unwrap() and expect()
.unwrap() extracts the Ok/Some value directly โ or panics if it's actually Err/None. .expect("message") does the same, with a custom panic message. Both are genuinely useful for quick prototypes or cases that are truly certain to succeed โ but reaching for them on an operation that could realistically fail in production turns a recoverable situation into a full program crash.
? is best understood as a more concise expression of the exact same idea Go's repeated if err != nil already embodies โ not a fundamentally different philosophy.
.unwrap() everywhere during early development, since it's shorter than a full match. The real risk: any call that genuinely can fail in production (parsing user input, a network request, a file read) will crash the entire program the moment it does, instead of being handled gracefully. Reserve bare .unwrap() for prototypes, tests, or situations that are truly structurally impossible to fail โ everywhere else, use match, ?, or at minimum .expect("a clear message") so a failure at least explains itself before the crash.
Coding Challenges
Write a function parse_age(input: &str) -> Result<u8, String> that attempts to parse a string into a u8, returning a clear error message on failure. Call it with both a valid and an invalid input, handling both with match.
Rewrite this chapter's calculate function (which chains two divide calls using ?) as if Rust had no ? operator at all โ using explicit match statements for each call instead. Compare the length and readability to the ? version.
๐ View solutionExplain when panic! is the right choice versus Result, using one concrete example of each โ and explain what's risky about calling .unwrap() on a Result that comes from parsing user-provided input.
๐ View solutionChapter 7 Quick Reference
- Result<T, E> โ Ok(T) for success, Err(E) for failure with an explanatory value
- panic! โ for unrecoverable bugs/invariant violations, stops the program immediately
- Result โ for expected, recoverable failures (bad input, a missing file, a failed request)
- ? โ propagates an Err immediately, or unwraps Ok and continues; the concise version of Go's repeated
if err != nil - .unwrap() / .expect("msg") โ extract the value or panic; fine for prototypes/tests, risky on anything that can genuinely fail in production
- Go and Rust share a real philosophy: errors as explicit return values, not exceptions
- Next chapter: Collections & Iterators โ Vec<T>, HashMap<K,V>, iterator adapters, and ownership implications of iterating
Collections & Iterators
Chapter 2's arrays were fixed-size. This final chapter of Course 1 covers Rust's growable collections โ and the iterator adapters that process them in a style that will feel immediately familiar coming from JavaScript.
Vec<T>: A Growable Array
Vec<T> can grow and shrink at runtime, backed by a heap allocation โ genuinely similar to Go's own slice type, which is Go's own answer to "an array that can grow." This is a real similarity worth noting, not just another contrast.
HashMap<K, V>: Key-Value Storage
get returns Option<&V> โ Chapter 6's Option making a direct return appearance. Go's built-in map[string]int is more tightly woven into the language's own syntax, and its lookup returns a (value, ok bool) pair โ both languages are explicit about "this key might not exist," just expressed through different mechanisms (Go's two-value return vs. Rust's Option).
Iterator Adapters: map, filter, collect
Rust's iterator methods will feel immediately familiar coming from JavaScript's own array methods โ the naming and behavior are genuinely close.
.iter() creates an iterator of references (borrowing, Chapter 4) without consuming numbers. .filter() and .map() are lazy โ nothing runs until .collect() actually consumes the chain and builds a new Vec.
| JavaScript | Rust | |
|---|---|---|
| Transform each element | array.map(x => ...) | .iter().map(|x| ...) |
| Keep matching elements | array.filter(x => ...) | .iter().filter(|x| ...) |
| Execution | Eager โ runs immediately | Lazy โ runs only when collected/consumed |
Ownership Implications of Iterating
Rust has three distinct ways to iterate, and choosing the wrong one is a genuinely common point of confusion:
| Method | Yields | Collection Usable After? |
|---|---|---|
.iter() | &T (borrowed references) | Yes |
.iter_mut() | &mut T (mutable references) | Yes |
.into_iter() / bare for x in vec | T (owned values) | No โ consumed/moved (Ch.3) |
Neither Go nor JavaScript has this distinction at all โ their iteration never "consumes" the underlying collection the way into_iter() does.
Vec<T> and HashMap<K,V> are both heap-allocated โ meaning Chapter 3's move semantics apply to them exactly as they did to String. Assigning one Vec to another variable moves it, not copies it; .clone() is still the explicit way to get a real independent duplicate. Every chapter in this course has been building toward being able to read code like this one's filter/map/collect chain and immediately know exactly what's borrowed, what's owned, and what's been moved.
collect() is generic over what it builds โ it needs to know the target type from somewhere, either an explicit variable type annotation (let x: Vec<i32> = ...) or the "turbofish" syntax (.collect::<Vec<i32>>()). Omitting both produces a real, common compiler error demanding a type annotation โ not a bug in the code's logic, just collect() genuinely being unable to infer what to build without more information.
Coding Challenges
Create a Vec of five integers. Using an iterator chain (.iter(), .filter(), .map(), .collect()), produce a new Vec containing only the odd numbers, each squared.
๐ View solutionCreate a HashMap mapping three product names (String) to their prices (f64). Look up one product that exists and one that doesn't, handling both cases with match on the Option returned by get.
๐ View solutionExplain the difference between iterating with .iter(), .iter_mut(), and into_iter() (or a bare for x in vec), specifically in terms of what happens to the original collection afterward in each case.
๐ View solutionChapter 8 Quick Reference
- Vec<T> โ a growable, heap-backed array;
vec![...]macro orVec::new()+.push() - HashMap<K, V> โ key-value storage;
.get()returnsOption<&V> - .iter().map(...).filter(...).collect() โ a lazy, chainable iterator pipeline, close to JavaScript's array methods
- .iter() / .iter_mut() โ borrow (immutably/mutably); the collection remains usable afterward
- .into_iter() / bare
for x in vecโ consumes the collection; it's moved and unusable afterward - collect() needs a target type โ via variable annotation or the turbofish
::<Type>() - Vec and HashMap are heap-allocated โ Chapter 3's move semantics apply to both directly
โ Rust Fundamentals Complete โ 8 / 8 chapters
From cargo and the toolchain through variables, ownership, borrowing, structs, enums, error handling, and finally collections and iterators โ you now have the complete foundation Rust's whole design is built on. Next up: Rust Intermediate, covering lifetimes, traits, generics, smart pointers, concurrency, modules, and testing.