I/O #
Operasi I/O di Rust diorganisasi dalam beberapa lapisan yang saling melengkapi. std::io mendefinisikan trait Read dan Write sebagai abstraksi universal — kode yang bekerja dengan file bisa bekerja dengan socket atau buffer di memori tanpa modifikasi. std::fs menyediakan fungsi tingkat tinggi untuk operasi file dan direktori. Dan std::path menangani path lintas platform dengan benar. Artikel ini membahas semua lapisan ini — dari membaca stdin hingga operasi file yang efisien dengan buffering, manajemen direktori, dan sekilas async I/O dengan tokio.
Trait Read dan Write — Abstraksi Inti I/O
#
Sebelum masuk ke file konkret, penting memahami dua trait yang menjadi fondasi seluruh ekosistem I/O Rust:
flowchart LR
R["trait Read\n.read(&mut [u8])\n.read_to_string()\n.read_to_end()\n.bytes()"]
W["trait Write\n.write(&[u8])\n.write_all(&[u8])\n.flush()\n.write_fmt()"]
R --> F1["File"]
R --> S1["TcpStream"]
R --> B1["&[u8] (slice)"]
R --> C1["Cursor<Vec<u8>>"]
W --> F2["File"]
W --> S2["TcpStream"]
W --> B2["Vec<u8>"]
W --> C2["BufWriter<W>"]Fungsi yang menerima impl Read atau impl Write bekerja dengan semua implementornya — testability langsung meningkat karena kamu bisa mengganti file dengan Vec<u8> saat testing.
Stdin dan Stdout #
Membaca Input dari Pengguna #
use std::io::{self, BufRead, Write};
fn main() {
// Cara 1: read_line — baca satu baris termasuk newline
print!("Masukkan nama kamu: ");
io::stdout().flush().unwrap(); // wajib flush sebelum baca input
let mut nama = String::new();
io::stdin().read_line(&mut nama).expect("Gagal membaca input");
let nama = nama.trim(); // hapus newline di akhir
println!("Halo, {}!", nama);
// Cara 2: baca banyak baris sampai EOF (Ctrl+D/Ctrl+Z)
println!("Masukkan teks (Ctrl+D untuk selesai):");
let stdin = io::stdin();
for baris in stdin.lock().lines() {
let baris = baris.expect("Gagal membaca baris");
println!("→ {}", baris.to_uppercase());
}
}
Membaca Input dan Parsing #
use std::io;
fn baca_angka(prompt: &str) -> i32 {
loop {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
match input.trim().parse::<i32>() {
Ok(n) => return n,
Err(_) => println!("Bukan angka valid, coba lagi."),
}
}
}
fn main() {
let a = baca_angka("Masukkan angka pertama: ");
let b = baca_angka("Masukkan angka kedua: ");
println!("{} + {} = {}", a, b, a + b);
}
Stderr untuk Pesan Error #
use std::io::{self, Write};
fn main() {
// println! → stdout (bisa di-redirect ke file)
println!("Ini output normal");
// eprintln! → stderr (untuk pesan error dan diagnostik)
eprintln!("Ini pesan error");
eprintln!("Error: file tidak ditemukan");
// Bisa juga tulis langsung ke stderr
let stderr = io::stderr();
writeln!(&mut stderr.lock(), "Error detail: {}", "koneksi timeout").unwrap();
}
Operasi File #
Baca dan Tulis Sederhana #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Tulis file — buat baru atau timpa jika sudah ada
fs::write("catatan.txt", "Baris pertama\nBaris kedua\nBaris ketiga\n")?;
println!("File berhasil ditulis");
// Baca seluruh file sebagai String
let isi = fs::read_to_string("catatan.txt")?;
println!("Isi file:\n{}", isi);
// Baca sebagai bytes (untuk file biner)
let bytes = fs::read("catatan.txt")?;
println!("Ukuran: {} byte", bytes.len());
// Hapus file
fs::remove_file("catatan.txt")?;
println!("File dihapus");
Ok(())
}
OpenOptions — Kontrol Penuh atas Mode Pembukaan File
#
Fungsi fs::write dan fs::read_to_string adalah shortcut yang nyaman, tapi untuk kontrol penuh gunakan OpenOptions:
use std::fs::OpenOptions;
use std::io::{self, Write, Read};
fn main() -> io::Result<()> {
// Buat file baru — gagal jika sudah ada
let mut file = OpenOptions::new()
.write(true)
.create_new(true) // error jika file sudah ada
.open("baru.txt")?;
writeln!(file, "Konten awal")?;
// Append — tambah di akhir tanpa hapus isi lama
let mut file = OpenOptions::new()
.append(true)
.open("baru.txt")?;
writeln!(file, "Baris tambahan")?;
writeln!(file, "Baris lagi")?;
// Baca dan tulis sekaligus
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open("baru.txt")?;
let mut isi = String::new();
file.read_to_string(&mut isi)?;
println!("Isi: {}", isi);
// Buat atau truncate (timpa jika ada)
let mut file = OpenOptions::new()
.write(true)
.create(true) // buat jika belum ada
.truncate(true) // kosongkan jika sudah ada
.open("baru.txt")?;
writeln!(file, "Isi baru")?;
fs::remove_file("baru.txt")?;
Ok(())
}
| Method | Efek |
|---|---|
.read(true) | Izinkan baca |
.write(true) | Izinkan tulis |
.append(true) | Tulis di akhir file |
.create(true) | Buat jika belum ada |
.create_new(true) | Buat, error jika sudah ada |
.truncate(true) | Kosongkan file saat dibuka |
BufReader dan BufWriter — I/O yang Efisien
#
Setiap panggilan read() atau write() ke file adalah syscall ke OS — mahal. BufReader mengumpulkan banyak byte dalam buffer memori dan melakukan satu syscall besar, BufWriter menahan data sampai buffer penuh atau di-flush. Untuk file besar, perbedaan performa bisa dramatis.
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
fn salin_dan_transformasi(sumber: &str, tujuan: &str) -> io::Result<()> {
let file_masuk = File::open(sumber)?;
let file_keluar = File::create(tujuan)?;
// BufReader: baca dengan buffer 8KB default
let reader = BufReader::new(file_masuk);
// BufWriter: tulis dengan buffer 8KB default
let mut writer = BufWriter::new(file_keluar);
let mut nomor = 1;
for baris in reader.lines() {
let baris = baris?;
// Transformasi: tambah nomor baris dan ubah ke uppercase
writeln!(writer, "{:4}: {}", nomor, baris.to_uppercase())?;
nomor += 1;
}
// Wajib flush BufWriter — data dalam buffer belum tentu tertulis ke disk
writer.flush()?;
println!("Berhasil menyalin {} baris", nomor - 1);
Ok(())
}
fn main() -> io::Result<()> {
// Buat file contoh
fs::write("input.txt", "baris satu\nbaris dua\nbaris tiga\n")?;
salin_dan_transformasi("input.txt", "output.txt")?;
let hasil = fs::read_to_string("output.txt")?;
println!("Hasil:\n{}", hasil);
fs::remove_file("input.txt")?;
fs::remove_file("output.txt")?;
Ok(())
}
use std::fs;
Selalu panggilwriter.flush()setelah selesai menulis melaluiBufWriter. Data dalam buffer bisa hilang jika program berakhir atau crash sebelum buffer di-flush. Drop dariBufWritermemang otomatis flush, tapi error saat drop diabaikan — flush eksplisit memungkinkan error handling yang proper.
BufReader dengan Ukuran Buffer Kustom
#
use std::fs::File;
use std::io::BufReader;
fn main() {
let file = File::open("data-besar.txt").unwrap();
// Buffer 64KB untuk file besar
let reader = BufReader::with_capacity(64 * 1024, file);
// Buffer kecil untuk file kecil yang sering dibaca
let file2 = File::open("config.txt").unwrap();
let reader2 = BufReader::with_capacity(512, file2);
}
Operasi Direktori #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Buat direktori
fs::create_dir("direktori-baru")?;
// Buat direktori beserta parent yang mungkin belum ada
fs::create_dir_all("a/b/c/d")?;
// Buat beberapa file di dalamnya
fs::write("direktori-baru/file1.txt", "konten 1")?;
fs::write("direktori-baru/file2.rs", "fn main() {}")?;
fs::write("direktori-baru/data.json", "{}")?;
// Baca isi direktori
println!("Isi direktori-baru:");
for entri in fs::read_dir("direktori-baru")? {
let entri = entri?;
let path = entri.path();
let meta = entri.metadata()?;
println!(
" {} ({}, {} byte)",
path.display(),
if meta.is_dir() { "direktori" } else { "file" },
meta.len()
);
}
// Salin file
fs::copy("direktori-baru/file1.txt", "direktori-baru/file1_backup.txt")?;
// Rename / pindahkan
fs::rename("direktori-baru/file2.rs", "direktori-baru/main.rs")?;
// Hapus file
fs::remove_file("direktori-baru/data.json")?;
// Hapus direktori kosong
// fs::remove_dir("direktori-baru")?; // error jika tidak kosong
// Hapus direktori beserta isinya
fs::remove_dir_all("direktori-baru")?;
fs::remove_dir_all("a")?;
println!("Selesai");
Ok(())
}
Rekursi Direktori — Walk Directory Tree #
Standard library tidak menyediakan recursive directory walk. Untuk itu gunakan crate walkdir:
[dependencies]
walkdir = "2"
use walkdir::WalkDir;
fn main() {
// Walk semua file dan direktori secara rekursif
for entri in WalkDir::new(".").into_iter().filter_map(|e| e.ok()) {
let path = entri.path();
let kedalaman = entri.depth();
let indent = " ".repeat(kedalaman * 2);
if path.is_dir() {
println!("{}{}/", indent, path.file_name().unwrap().to_string_lossy());
} else {
println!("{}{}", indent, path.file_name().unwrap().to_string_lossy());
}
}
// Filter hanya file dengan ekstensi tertentu
let file_rs: Vec<_> = WalkDir::new("src")
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|ext| ext == "rs").unwrap_or(false))
.collect();
println!("\nFile .rs ditemukan: {}", file_rs.len());
}
std::path — Manajemen Path Lintas Platform
#
Jangan pernah konkatenasi path dengan string biasa — gunakan Path dan PathBuf yang menangani perbedaan separator (/ vs \) secara otomatis:
use std::path::{Path, PathBuf};
use std::fs;
fn main() {
// PathBuf: owned, bisa dimodifikasi
let mut path = PathBuf::from("/home/budi");
path.push("dokumen");
path.push("laporan.pdf");
println!("{}", path.display()); // /home/budi/dokumen/laporan.pdf
// Path: borrowed, tidak bisa dimodifikasi (seperti &str vs String)
let path_ref: &Path = Path::new("/etc/hosts");
// Komponen path
println!("Nama file: {:?}", path.file_name()); // "laporan.pdf"
println!("Ekstensi: {:?}", path.extension()); // "pdf"
println!("Stem: {:?}", path.file_stem()); // "laporan"
println!("Parent: {:?}", path.parent()); // /home/budi/dokumen
// Gabungkan path dengan join
let base = Path::new("/var/log");
let log_file = base.join("aplikasi").join("error.log");
println!("{}", log_file.display()); // /var/log/aplikasi/error.log
// Cek status path
let p = Path::new("Cargo.toml");
println!("Ada: {}", p.exists());
println!("File: {}", p.is_file());
println!("Direktori: {}", p.is_dir());
// Path absolut dari path relatif
if let Ok(absolut) = p.canonicalize() {
println!("Absolut: {}", absolut.display());
}
}
Menulis ke Vec<u8> — I/O In-Memory
#
Karena Vec<u8> mengimplementasikan Write, kamu bisa menggunakan API tulis yang sama untuk menyimpan output ke memori alih-alih file — sangat berguna untuk testing:
use std::io::{self, Write, Cursor};
fn tulis_laporan(writer: &mut impl Write) -> io::Result<()> {
writeln!(writer, "=== LAPORAN ===")?;
writeln!(writer, "Total item: {}", 42)?;
writeln!(writer, "Status: OK")?;
Ok(())
}
fn main() -> io::Result<()> {
// Tulis ke file
let mut file = std::fs::File::create("laporan.txt")?;
tulis_laporan(&mut file)?;
// Tulis ke memori (berguna untuk test atau buffer)
let mut buffer: Vec<u8> = Vec::new();
tulis_laporan(&mut buffer)?;
println!("Output:\n{}", String::from_utf8_lossy(&buffer));
// Cursor<Vec<u8>>: bisa Read DAN Write
let mut cursor = Cursor::new(Vec::new());
writeln!(cursor, "Data yang bisa dibaca dan ditulis")?;
cursor.set_position(0); // rewind ke awal
let mut hasil = String::new();
use std::io::Read;
cursor.read_to_string(&mut hasil)?;
println!("Dari cursor: {}", hasil);
std::fs::remove_file("laporan.txt")?;
Ok(())
}
Async I/O dengan Tokio #
Untuk aplikasi yang menangani banyak koneksi bersamaan atau I/O yang berpotensi lambat, async I/O mencegah pemblokiran thread:
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::fs;
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main]
async fn main() -> io::Result<()> {
// Tulis file secara async
fs::write("async_test.txt", "baris satu\nbaris dua\nbaris tiga").await?;
// Baca file secara async
let isi = fs::read_to_string("async_test.txt").await?;
println!("Isi:\n{}", isi);
// Baca baris per baris secara async
let file = fs::File::open("async_test.txt").await?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
while let Some(baris) = lines.next_line().await? {
println!("Baris: {}", baris);
}
// Tulis secara async dengan append
let mut file = fs::OpenOptions::new()
.append(true)
.open("async_test.txt")
.await?;
file.write_all(b"\nbaris keempat (async)").await?;
// Metadata async
let meta = fs::metadata("async_test.txt").await?;
println!("Ukuran: {} byte", meta.len());
fs::remove_file("async_test.txt").await?;
Ok(())
}
Ringkasan #
- Trait
ReaddanWriteadalah abstraksi inti — kode yang menerimaimpl Read/impl Writebekerja dengan file, socket,Vec<u8>, dan semua tipe lain yang mengimplementasikan trait tersebut.fs::read_to_stringdanfs::writeuntuk kasus sederhana — shortcut yang nyaman untuk baca/tulis seluruh file sekaligus.OpenOptionsuntuk kontrol penuh — pilih kombinasiread,write,append,create,create_new,truncatesesuai kebutuhan.- Selalu gunakan
BufReader/BufWriteruntuk file besar — mengurangi syscall secara drastis. Tanpa buffering, setiapread_line()adalah syscall terpisah.- Wajib
flush()setelahBufWriter— data dalam buffer belum tentu tertulis ke disk. Drop otomatis flush tapi error-nya tidak bisa ditangkap.- Gunakan
Path/PathBuf, bukan string untuk path — menangani separator platform secara otomatis dan menyediakan API ergonomis untuk join, extension, parent, dll.Vec<u8>mengimplementasikanWrite— tulis ke buffer memori menggunakan API yang sama persis dengan file. Sangat berguna untuk testing dan caching output.- Untuk recursive directory walk, gunakan crate
walkdir— standard library tidak menyediakan ini.- Async I/O (tokio) untuk server dan high-concurrency —
tokio::fsmenyediakan API yang identik denganstd::fstapi non-blocking.