[Rust] Reading Input and Writing Output

When solving algorithm problems in Rust, I/O is usually the first wall you hit. It's not as straightforward as cin/cout in C++ or input()/print() in Python, but once you learn it, you can use the same pattern everywhere.

Basic Output: println!

Rust
fn main() {
    println!("Hello World!");
}

The simplest way to print. Because stdout is flushed line by line and println! takes the lock on every call, it gets slow when you have a lot of output.

Fast Output: BufWriter

Rust
use std::io::{self, Write, BufWriter};

fn main() {
    let stdout = io::stdout();
    let mut out = BufWriter::new(stdout.lock());
    writeln!(out, "Hello World!").unwrap();
}

Wrapping stdout in a BufWriter buffers the output and writes it out in larger chunks. For problems with heavy output, the difference is large. Whatever is left in the buffer is flushed when out is dropped (usually at the end of main).

  • stdout.lock() → acquires the stdout lock only once
  • BufWriter → reduces the number of system calls via buffering
  • writeln! → the macro to use instead of println!

Basic Input: read_line

Rust
use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    let n: i32 = input.trim().parse().unwrap();
    println!("{}", n);
}

Fine for reading a single line, but tedious when you need to read multiple lines.

Fast Input: BufReader + read_to_string

Rust
use std::io::{self, BufReader, Read};

fn main() {
    let mut input = String::new();
    let mut reader = BufReader::new(io::stdin().lock());
    reader.read_to_string(&mut input).unwrap();

    let mut iter = input.split_whitespace();
    let n: i32 = iter.next().unwrap().parse().unwrap();
}

read_to_string reads the entire input at once, up to EOF. When running directly in a terminal, you'll need to press Ctrl+Z then Enter (Windows) or Ctrl+D (Linux/Mac) after entering your input to signal EOF. On online judges like BOJ, EOF is sent automatically, so you don't need to worry about it.

Note that stdin already has an internal buffer, so io::stdin().read_to_string(&mut input) works without BufReader.

split_whitespace() treats spaces, tabs, and newlines all as delimiters, making it easy to parse token by token.

Reading Two Numbers and Adding Them (BOJ 1000)

Rust
use std::io::{self, BufReader, BufWriter, Read, Write};

fn main() {
    let mut input = String::new();
    let mut reader = BufReader::new(io::stdin().lock());
    reader.read_to_string(&mut input).unwrap();

    let mut iter = input.split_whitespace();
    let a: i32 = iter.next().unwrap().parse().unwrap();
    let b: i32 = iter.next().unwrap().parse().unwrap();

    let stdout = io::stdout();
    let mut out = BufWriter::new(stdout.lock());
    writeln!(out, "{}", a + b).unwrap();
}

What Is unwrap()?

writeln! and parse() return a Result type.

  • Success (Ok(value)) → unwrap() extracts the value
  • Failure (Err(error)) → unwrap() panics and terminates the program

In algorithm problems, the input format is fixed, so using unwrap() to keep things simple is fine. In production code, it's better to propagate errors with the ? operator.

What Are mut and &mut?

mut in variable declarations

In Rust, variables are immutable by default. You need mut to make them changeable.

Rust
let input = String::new();       // immutable → cannot be modified
let mut input = String::new();   // mutable → can be modified

Since read_to_string needs to write data into input, mut is required.

&mut when passing to functions

& is a reference — it borrows a value without copying it.

  • &input → read-only reference (immutable reference)
  • &mut input → read + write reference (mutable reference)
Rust
reader.read_to_string(&mut input).unwrap();
//                    ^^^^^^^^^^
// We pass &mut because read_to_string needs to write into input

Since read_to_string needs to fill input with the data it reads, it requires a mutable reference via &mut.

Why Call lock()?

stdout() and stdin() can be accessed from multiple threads simultaneously, so by default they acquire and release a lock on every call.

Rust
// println! works roughly like this internally
stdout().lock() → write → unlock
stdout().lock() → write → unlock  // slower the more times it runs

Calling .lock() directly lets you hold the lock once and keep using it.

Rust
let stdout = io::stdout();
let locked = stdout.lock();           // lock acquired once
let mut out = BufWriter::new(locked); // reused from here on

The same applies to stdin. Calling stdin().lock() once means no repeated lock/unlock cycles while reading, which is faster.

What Is next()?

split_whitespace() returns an iterator — an object that lets you pull values out one at a time, in order.

Each call to .next() returns the next value.

Rust
let input = "1 2 3";
let mut iter = input.split_whitespace();

iter.next()  // → Some("1")  first value
iter.next()  // → Some("2")  second value
iter.next()  // → Some("3")  third value
iter.next()  // → None       nothing left

Some and None are the Option type — they represent a value that may or may not exist. unwrap() extracts the value inside Some.

Rust
iter.next()           // Some("1")
iter.next().unwrap()  // "1" ← the value unwrapped from Some

What Is parse()?

parse() converts a string into a desired type. It's the equivalent of int() in Python or stoi() in C++.

Rust
let s = "42";
let n: i32 = s.parse().unwrap();  // "42" → 42 (i32)

The target type is inferred from the variable's type annotation.

Rust
let a: i32 = "42".parse().unwrap();    // 32-bit integer
let b: i64 = "42".parse().unwrap();    // 64-bit integer
let c: f64 = "3.14".parse().unwrap();  // 64-bit float
let d: usize = "42".parse().unwrap();  // unsigned integer (for array indices, etc.)

If the conversion fails, it returns Err, and unwrap() will panic.

Rust
let n: i32 = "abc".parse().unwrap();  // panic! not a number

Putting It All Together

Here's a step-by-step breakdown of parsing the input "1 2" into two integers:

Rust
let mut iter = input.split_whitespace();
// iter: an iterator that yields "1" and "2" in order

let a: i32 = iter.next().unwrap().parse().unwrap();
//           ^^^^^^^^^^^  → Some("1")
//                        ^^^^^^^^^  → "1"
//                                    ^^^^^^^  → Result<i32, _>
//                                              ^^^^^^^^^  → 1 (i32)

Pull out a string with next() → unwrap Some with unwrap() → convert to a number with parse() → unwrap Result with unwrap().

Summary

SlowFast
Inputstdin().read_line()BufReader + read_to_string
Outputprintln!BufWriter + writeln!

For competitive programming, just always use the BufReader + BufWriter combination.