
Rust: Safe and Efficient File I/O with the Standard Library
In Rust, file operations are typically handled using the std::fs module for high-level functions and std::io for more granular control over streams. These modules provide abstractions for common tasks such as reading lines, writing data, and managing file handles in a safe and efficient manner.
This tutorial covers:
- Reading the contents of a file
- Writing to a file (appending and overwriting)
- Iterating over lines in a file
- Handling I/O errors using
Resultand?
Let’s dive into concrete examples and best practices.
Reading a File
To read the entire contents of a file, the std::fs::read_to_string function is the most straightforward option. It returns a Result<String, std::io::Error> and automatically closes the file after reading.
use std::fs;
fn read_file() -> std::io::Result<()> {
let contents = fs::read_to_string("example.txt")?;
println!("File contents:\n{}", contents);
Ok(())
}For larger files or when you need more control, use File and BufReader:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn read_file_with_buffer() -> std::io::Result<()> {
let file = File::open("example.txt")?;
let reader = BufReader::new(file);
for (index, line) in reader.lines().enumerate() {
let line = line?;
println!("Line {}: {}", index + 1, line);
}
Ok(())
}Using BufReader improves performance by reducing the number of system calls.
Writing to a File
Writing to a file can be done using std::fs::write, which replaces the file contents, or std::fs::OpenOptions for more control over write modes (e.g., appending).
use std::fs;
fn write_file() -> std::io::Result<()> {
let data = "Hello from Rust!\n";
fs::write("output.txt", data)?;
Ok(())
}To append to a file instead of overwriting it:
use std::fs::OpenOptions;
use std::io::Write;
fn append_to_file() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open("output.txt")?;
file.write_all(b"Appended line\n")?;
Ok(())
}Error Handling
Proper error handling is essential in Rust. The ? operator propagates errors by returning early if an Err variant is encountered. This ensures that functions that return Result types can be composed cleanly.
Compare the following two approaches:
| Method | Description | Example |
|---|---|---|
unwrap() | Panics on error | let data = fs::read_to_string("file.txt").unwrap(); |
? operator | Returns Err on error | let data = fs::read_to_string("file.txt")?; |
Always prefer ? in functions that return Result or Option to avoid panics in production code.
Idiomatic File Iteration
When reading a file line by line, using BufRead::lines() with for loops is idiomatic and efficient.
use std::fs::File;
use std::io::{BufRead, BufReader};
fn process_lines() -> std::io::Result<()> {
let file = File::open("input.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
// Process the line
if line.contains("error") {
println!("Found error in line: {}", line);
}
}
Ok(())
}This pattern is especially useful for log processing or parsing structured text.
Summary of Common File I/O Operations
| Task | Function | Description |
|---|---|---|
| Read entire file | fs::read_to_string | Reads file into a String |
| Write to file | fs::write | Overwrites file with given data |
| Open for writing | OpenOptions::new().write(true).open() | Opens file for writing |
| Append to file | OpenOptions::new().append(true).open() | Appends data to file |
| Read line by line | BufRead::lines() | Iterates over lines of a file |
