Programming a Guessing Game
A complete program in one chapter: I/O, Result, external crates, match, and shadowing.
Part of Rust Interview Mastery · Based on The Rust Programming Language
Input, Result & Associated Functions
Walk me through reading a line of user input in Rust.
let mut guess = String::new();
std::io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
String::new() creates an empty, growable UTF-8 buffer — new is an associated function (a constructor convention, called on the type with ::, not on an instance). read_line(&mut guess) takes a mutable reference so it can append bytes without taking ownership. It returns Result<usize> — bytes read, or an error.
What is Result and why do we call .expect() everywhere in chapter 2?
Result<T, E> is an enum — Ok(T) on success, Err(E) on failure. Rust has no exceptions; fallible operations return Result and the compiler forces you to acknowledge it.
expect("msg") is the blunt instrument: unwrap Ok, or panic! with your message on Err. Chapter 2 uses it deliberately — a toy program crashing on I/O failure is acceptable. Production code propagates with ? instead, which chapter 9 formalizes.
Why does read_line need &mut guess instead of returning a String?
Two reasons. First, appending into a caller-provided buffer lets you reuse the allocation across calls. Second, returning Result<usize> frees the return slot for the real status — the bytes-read count — instead of conflating data with status.
It's the first place the book shows Rust's signature style: mutability is explicit in the type (&mut), and outputs stay separate from error channels.
Crates, Match & Shadowing
How do you add and use an external crate like rand?
Add it under [dependencies] in Cargo.toml: rand = "0.9". On the next build, Cargo fetches it from crates.io plus its transitive deps, records exact versions in Cargo.lock, and compiles it.
The version string is semver: "0.9" means ^0.9 — any 0.9.x compatible release. Then in code: let n = rand::rng().random_range(1..=100); — an inclusive range yielding the secret number.
How does the guessing game compare the guess to the secret number?
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
cmp returns an Ordering — Less, Greater, or Equal. match must be exhaustive: every variant needs an arm, or the code doesn't compile. That's the type system killing the "forgot a case" bug at compile time.
The tutorial uses shadowing to convert the guess to a number — explain it.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
The second let guess shadows the first — it's a brand-new binding that happens to reuse the name. The String from input is still there in memory, but inaccessible. Shadowing lets you transform a value (String → u32) without inventing guess_str / guess_num names, and it can change the type — mut cannot.
parse infers u32 from the annotation and returns Result, hence the expect.
How does the game loop handle invalid input, and why is that better than crashing?
The match on parse result separates the two paths:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
continue skips to the next loop iteration — the user just gets re-prompted instead of the process dying. It's a small preview of chapter 9's philosophy: decide where each error is handled, and handle recoverable ones locally.
What constructs from this chapter show up constantly in real Rust code?
- Result + expect/match error handling — everywhere I/O happens
- match exhaustiveness — the core pattern for enums, Options, and Results
- Shadowing for staged parsing — let x = parse(validate(raw)) style pipelines
- loop/break/continue for retry loops (network reconnects, menus, REPLs)
- use imports and :: paths for library calls
The guessing game is tiny but it exercises the five everyday primitives.