Coding Guessing Game
Adding guess features
Let's start modifying the src/main.rs file that was generated by cargo:
use std::io;
fn main() {
println!("guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
For the sake of brevity, I won't be explaining line by line on what this code does, more detail can be found in this link: https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html#processing-a-guess
However, I want to expand on few concepts (variables and stdin) in this page before moving forward
Variables
In the guessing game, we start by creating a variable to store user input:
let mut guess = String::new();
At first glance, this line may look straightforward. However, it introduces several important Rust concepts that are worth unpacking early.
For comparison, Rust variables can also be defined in much simpler forms:
let apples = 5;
Although both lines create variables, they behave very differently.
Understanding why will help clarify how Rust thinks about mutability, types, and intent.
The following sections expand on these ideas before we continue.
1. Immutable vs mutable variables
In Rust, immutability is the default (which aligns with Rust’s focus on correctness and memory safety).
Two kinds of bindings
let x = 5; // immutable binding
let mut y = 5; // mutable binding
An important thing to note is that immutability does not mean “the value is frozen forever”.
Instead, it means what operations are allowed through this name (x or y).
| Binding type | What you can do |
|---|---|
let x = ... | Read only |
let mut x = ... | Read and Write |
2. Why do we use String::new()?
String::new() is what is known as an associated function
Key properties:
Stringis a typenewis an associated function onString(function belong to a type)- Do not take
self
This results in a new, empty String value, which is useful when you need to explicitly create a value of type String (think of it like int c = 5; in c).
Some other examples are as follows:
String::new()
Vec::with_capacity(10)
i32::from_str("42")
3. Immutability is about binding, not the type
For example, this is valid:
let s = String::new(); // immutable String binding
let mut s = String::new(); // mutable String binding
However, this is not valid:
let s: mut String = String::new(); // ❌ Rust does not work this way
Because you may think this may work as we are creating a variable named s while defining the type of String that is mutable via mut, but mutability lives on the name, not on the type.
Okay, great, we have a mutable String, how does input actually get into it?
User Input
Recall this line of the code:
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
Let's break this line of code into three parts:
1. Getting a handle to standard input
std::io::stdin()
Key points:
stdin()is a free function defined in thestd::iomodule
A free function is a function that lives on its own inside a module. It does not belong to a struct or type, and it does not operate on
self.
- It returns a value of type
Stdin - A handle is a value that represents access to an external resource (in this case, the resource is standard input)
- Calling
stdin()does not read any input yet
At this stage, we are simply obtaining a safe way to interact with standard input.
To summarize:
| Kind | How it’s called | Example |
|---|---|---|
| Free function | module::function() | std::io::stdin() |
| Associated function | Type::function() | String::new() |
| Method | value.function() | stdin.read_line(...) |
Why stdin() is not an associated function
At first glance, it is easy to assume that stdin() is an associated function of Stdin, because it is called like this:
std::io::stdin()
However, this syntax does not imply an associated function.
Instead, stdin() is defined as a free function inside the std::io module, something like this:
mod io {
pub fn stdin() -> Stdin { ... }
}
Because of this:
stdin()belongs to the module, not theStdintype- You call it via the module path:
std::io::stdin() - The function simply returns a
Stdinvalue, which you can then use
2. Reading into an existing buffer
io::stdin().read_line(&mut guess)
Key points:
read_lineis a method on theStdintype- It requires a mutable reference (
&mut) to aString - It appends user input into the provided buffer (
guess) - It returns a value of type
Result<usize>
A few expansions on those key points:
1. read_line is a method
Once you have a Stdin value, you can call methods on it. read_line is one of them.
Its signature (simplified) looks like this:
impl Stdin {
pub fn read_line(&mut self, buf: &mut String) -> Result<usize> { ... }
}
This tells us:
buf: &mut Stringmeansread_lineneeds permission to mutate the providedString- Because
bufis a reference, theStringis borrowed, not moved (ownership is not transferred) - The function writes into the existing buffer you provide
- This is a method because it takes
self(by reference)
That’s why it is called on a Stdin value:
let stdin = std::io::stdin();
stdin.read_line(&mut guess);
2. What "appends input" means:
read_line does not replace the contents of the String buffer.
Instead, it adds the input to the end of the existing buffer (including the trailing newline).
3. What Result<usize> means
Conceptually:
enum Result<T, E> {
Ok(T),
Err(E),
}
This means:
-
The operation can either succeed or fail:
Ok(usize)means success, and the usize value represents the number of bytes readErr(io::Error)means something went wrong during input and must be handled
-
Rust forces you to handle both cases.
This naturally leads to how Rust handles failures explicitly—often using .expect(...) as we will see in the next section.
3. Handling failure explicitly with .expect()
std::io::stdin().expect("Failed to read line");
At this point, we know that read_line returns a Result<usize> — meaning the operation can succeed or fail.
Rust does not allow you to ignore that fact.
Why .expect() exists
In Rust, errors are values, not exceptions.
In other words:
- Functions that can fail must say so in their type
- Callers must explicitly decide what to do in both cases
The Result type forces you to handle failure at compile time.
.expect() is one of the simplest ways to do that.
What .expect() actually does
.expect() is a method defined on Result<T, E>.
Its behavior is:
- If the result is
Ok(T)→ extract and return the value - If the result is
Err(E)→ panic and crash the program while providing the message to explain why
Therefore, Rust makes error handling explicit by design.
You cannot:
- Accidentally ignore failures
- Forget that something might go wrong
- Assume success by default
Even in this simple example, Rust forces you to acknowledge:
“Reading input can fail — what should happen if it does?”
Phew! Two seemingly simple lines of code ended up introducing quite a few important Rust ideas.
In the next section, we’ll make the guessing game a bit more interesting.
For now, make sure to test your code by running:
» cargo run
Compiling ch2-guessing-game v0.1.0 (/home/jw-jang/rust-up-experiments/ch2-guessing-game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/ch2-guessing-game`
guess the number!
Please input your guess.
test
You guessed: test