Enums & Pattern Matching

Course 1 · Ch 6
Enums & Pattern Matching
Enums that actually carry data, an exhaustive match, and eliminating null as a concept entirely

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.

enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));

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.

fn describe(addr: &IpAddr) -> String { match addr { IpAddr::V4(a, b, c, d) => format!("IPv4: {}.{}.{}.{}", a, b, c, d), IpAddr::V6(s) => format!("IPv6: {}", s), } }

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:

enum Option<T> { Some(T), None, }

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.

fn safe_divide(a: f64, b: f64) -> Option<f64> { if b == 0.0 { None } else { Some(a / b) } } match safe_divide(10.0, 2.0) { Some(result) => println!("Result: {}", result), None => println!("Cannot divide by zero"), }
GoRust
"Enum"iota — just typed integer constantsVariants can each carry their own data
Missing valuenil / zero valueOption<T> — Some(T) or None
What happens if mishandledNil pointer dereference — a runtime panicCompile error — must handle None before compiling
Fixing "the billion dollar mistake"
Tony Hoare, who invented the null reference in 1965, later called it his "billion dollar mistake," estimating the cumulative cost of null-related bugs across the software industry. 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.
A broken match after adding a variant is the compiler helping you
Adding a new variant to an existing enum can suddenly make every 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

Challenge 1

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 solution
Challenge 2

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

📄 View solution
Challenge 3

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.

📄 View solution

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) or None
  • 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, and panic! vs. recoverable errors