I/O

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(())
}
MethodEfek
.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 panggil writer.flush() setelah selesai menulis melalui BufWriter. Data dalam buffer bisa hilang jika program berakhir atau crash sebelum buffer di-flush. Drop dari BufWriter memang 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());
    }
}

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 Read dan Write adalah abstraksi inti — kode yang menerima impl Read/impl Write bekerja dengan file, socket, Vec<u8>, dan semua tipe lain yang mengimplementasikan trait tersebut.
  • fs::read_to_string dan fs::write untuk kasus sederhana — shortcut yang nyaman untuk baca/tulis seluruh file sekaligus.
  • OpenOptions untuk kontrol penuh — pilih kombinasi read, write, append, create, create_new, truncate sesuai kebutuhan.
  • Selalu gunakan BufReader/BufWriter untuk file besar — mengurangi syscall secara drastis. Tanpa buffering, setiap read_line() adalah syscall terpisah.
  • Wajib flush() setelah BufWriter — 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> mengimplementasikan Write — 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-concurrencytokio::fs menyediakan API yang identik dengan std::fs tapi non-blocking.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact