Skip to main content

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 typeWhat 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:

  • String is a type
  • new is an associated function on String (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 the std::io module

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:

KindHow it’s calledExample
Free functionmodule::function()std::io::stdin()
Associated functionType::function()String::new()
Methodvalue.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 the Stdin type
  • You call it via the module path: std::io::stdin()
  • The function simply returns a Stdin value, which you can then use

2. Reading into an existing buffer

io::stdin().read_line(&mut guess)

Key points:

  • read_line is a method on the Stdin type
  • It requires a mutable reference (&mut) to a String
  • 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 String means read_line needs permission to mutate the provided String
  • Because buf is a reference, the String is 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 read
    • Err(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