Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
99: BufReader and BufWriter
Why should I bother wrapping my files in BufReader?
You might think that calling read on a std::fs::File is as efficient as it gets, but here is the catch: every single time you make a system call to the OS to read a chunk of bytes, there's a performance penalty. If you're reading a large file one byte at a time, or even in small 10-byte chunks, your program will spend more time talking to the kernel than actually processing data.
I've seen this bite people when they start writing custom parsers. BufReader solves this by reading a large chunk of the file (usually 8KB) into an internal memory buffer in one go. When you ask for the next byte, Rust just hands it to you from that memory buffer. It's orders of magnitude faster because it minimizes those expensive trips to the hard drive.
How do I actually use this to read a file line-by-line?
This is probably the most common use case for BufReader. To get the lines() method, you need to import the BufRead trait. If you forget that import, you'll spend ten minutes wondering why the method doesn't exist on your reader.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> {
let file = File::open("server_logs.txt")?;
// Wrap the file in a BufReader
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.contains("ERROR") {
println!("Found an error: {}", line);
}
}
Ok(())
}
Notice how we wrap the File object. The BufReader essentially "owns" the file now and manages the buffering logic for you.
What is the deal with BufWriter and flushing?
BufWriter is the mirror image of BufReader. Instead of reading a big chunk from the disk, it collects your small writes in memory and only sends them to the OS when the buffer is full. It's a lifesaver when you're writing thousands of small strings to a file.
But there's a trap: because the data lives in a buffer, the last bit of your data might still be sitting in memory when your program finishes. Usually, when the BufWriter is dropped, it attempts to flush that data to disk. However, if a panic occurs or you need to ensure the data is written immediately before the writer goes out of scope, you must call .flush() manually.
use std::fs::File;
use std::io::{self, BufWriter, Write};
fn save_data() -> io::Result<()> {
let file = File::create("output.dat")?;
let mut writer = BufWriter::new(file);
writer.write_all(b"Some important data\n")?;
writer.write_all(b"More important data\n")?;
// I always recommend an explicit flush if the writer
// lives for a long time or handles critical data.
writer.flush()?;
Ok(())
}
If you're just writing a small CLI tool, you can often get away without the explicit flush, but in production systems, it's a habit that will save you from "missing data" bugs that are a nightmare to debug.
📋 Practical Task
Exercise: Buffered Log Filtering Tool
Your task is to create a program that reads a large log file (input.log) and writes only the lines containing the keyword "CRITICAL" into a new file (critical_errors.log). To make this performant, you must use both BufReader and BufWriter.
- Create a dummy
input.logfile with several lines of text, some containing "INFO", "WARN", and "CRITICAL". - Use
BufReader::new()to read the input file line-by-line. - Use
BufWriter::new()to write the filtered lines to the output file. - Ensure you call
.flush()on your writer before the function returns to ensure all critical errors are safely saved to disk.
There are no comments for now.