BAB 44: Operasi File — Baca, Tulis, dan Kelola File

Pelajari cara membuat, menulis, membaca, dan menghapus file di Go menggunakan package os, termasuk penanganan error dan pola idiomatis dengan defer.

Di bab sebelumnya, program Go sudah bisa menjalankan command eksternal dan membaca outputnya. Tapi orkestrator yang benar-benar berguna sering butuh lebih dari sekadar memanggil binary lain — ia perlu menulis hasil ke file, membaca konfigurasi dari disk, atau membersihkan file sementara setelah selesai bekerja. Singkatnya, ia perlu berbicara langsung dengan sistem file.

Go menyediakan kemampuan ini melalui package os — bagian dari standard library yang menjadi jembatan antara program dan sistem operasi. Tidak perlu library eksternal.

Membuat File Baru

Fungsi os.Create() membuat file baru dan langsung mengembalikan handle-nya. Jika file sudah ada, isinya akan dikosongkan dan file dibuka ulang.

// filemanager.go
package main

import (
    "fmt"
    "os"
)

func buatFile(nama string) error {
    _, err := os.Stat(nama)
    if err == nil {
        return fmt.Errorf("file %q sudah ada", nama)
    }
    if !os.IsNotExist(err) {
        return fmt.Errorf("tidak bisa memeriksa file: %w", err)
    }

    f, err := os.Create(nama)
    if err != nil {
        return fmt.Errorf("gagal membuat file: %w", err)
    }
    defer f.Close()

    fmt.Printf("file %q berhasil dibuat\n", nama)
    return nil
}

func main() {
    if err := buatFile("catatan.txt"); err != nil {
        fmt.Println("error:", err)
        return
    }

    // coba buat lagi — seharusnya ditolak
    if err := buatFile("catatan.txt"); err != nil {
        fmt.Println("error:", err)
    }
}
file "catatan.txt" berhasil dibuat
error: file "catatan.txt" sudah ada

Dua hal penting di sini: pertama, os.Stat() digunakan untuk memeriksa apakah file sudah ada sebelum mencoba membuatnya. Kedua, defer f.Close() memastikan handle file selalu ditutup saat fungsi selesai — pola ini wajib digunakan setiap kali membuka file, apapun alurnya.

os.IsNotExist(err) adalah cara idiomatis Go untuk memeriksa apakah error disebabkan oleh file yang tidak ditemukan. Hindari membandingkan pesan error secara string — gunakan fungsi helper dari package os agar kode tetap portabel di semua sistem operasi.

Setelah file ada, gunakan os.OpenFile() dengan flag yang tepat untuk membuka dan menulis ke dalamnya. os.O_WRONLY berarti write-only, os.O_APPEND berarti tambahkan di akhir tanpa menghapus isi lama.

// filemanager.go
package main

import (
    "fmt"
    "os"
)

func tulisFile(nama string, isi string) error {
    f, err := os.OpenFile(nama, os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("gagal membuka file untuk ditulis: %w", err)
    }
    defer f.Close()

    _, err = f.WriteString(isi)
    if err != nil {
        return fmt.Errorf("gagal menulis ke file: %w", err)
    }

    err = f.Sync()
    if err != nil {
        return fmt.Errorf("gagal sinkronisasi ke disk: %w", err)
    }

    return nil
}

func main() {
    entri := []string{
        "2026-03-13 09:00 — mulai kerja\n",
        "2026-03-13 10:30 — review pull request\n",
        "2026-03-13 12:00 — istirahat makan siang\n",
    }

    for _, baris := range entri {
        if err := tulisFile("catatan.txt", baris); err != nil {
            fmt.Println("error:", err)
            return
        }
    }

    fmt.Println("semua entri berhasil ditulis")
}
semua entri berhasil ditulis

f.Sync() memaksa sistem operasi untuk menuliskan data dari buffer ke disk fisik. Untuk program yang menganggap data penting, ini langkah yang tidak boleh dilewati — tanpa Sync(), ada risiko data hilang jika program crash sebelum buffer di-flush secara otomatis.

Kombinasi flag yang umum digunakan:

Kombinasi FlagEfek
O_WRONLY | O_CREATE | O_TRUNCBuat atau timpa file
O_WRONLY | O_APPENDTambah di akhir file yang ada
O_RDWRBaca dan tulis
O_RDONLYHanya baca (ini juga default os.Open)

Membaca dari File

Untuk membaca, os.Open() adalah shortcut dari os.OpenFile() dengan flag read-only. Baca isi file secara bertahap menggunakan buffer agar program tidak memuat seluruh file ke memori sekaligus.

// filemanager.go
package main

import (
    "fmt"
    "io"
    "os"
)

func bacaFile(nama string) error {
    f, err := os.Open(nama)
    if err != nil {
        return fmt.Errorf("gagal membuka file: %w", err)
    }
    defer f.Close()

    buf := make([]byte, 64)
    fmt.Printf("=== isi %q ===\n", nama)

    for {
        n, err := f.Read(buf)
        if n > 0 {
            fmt.Print(string(buf[:n]))
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("error saat membaca: %w", err)
        }
    }

    return nil
}

func main() {
    if err := bacaFile("catatan.txt"); err != nil {
        fmt.Println("error:", err)
    }
}
=== isi "catatan.txt" ===
2026-03-13 09:00 — mulai kerja
2026-03-13 10:30 — review pull request
2026-03-13 12:00 — istirahat makan siang

Loop for terus membaca sampai f.Read() mengembalikan io.EOF — sinyal bahwa seluruh isi file sudah dibaca. Perhatikan pemeriksaan n > 0 dilakukan sebelum memeriksa error: ini pola yang benar karena Read() bisa mengembalikan data dan io.EOF bersamaan di pembacaan terakhir.

Untuk file kecil yang keseluruhan isinya perlu dibaca sekaligus, os.ReadFile() adalah alternatif yang lebih ringkas. Tapi untuk file besar atau stream yang tidak terbatas ukurannya, pembacaan berbasis buffer seperti di atas adalah pilihan yang tepat.

Menghapus File

os.Remove() menghapus file dari filesystem. Satu pemanggilan, satu file.

// filemanager.go
package main

import (
    "fmt"
    "os"
)

func hapusFile(nama string) error {
    err := os.Remove(nama)
    if err != nil {
        if os.IsNotExist(err) {
            return fmt.Errorf("file %q tidak ditemukan", nama)
        }
        return fmt.Errorf("gagal menghapus file: %w", err)
    }
    fmt.Printf("file %q berhasil dihapus\n", nama)
    return nil
}

func main() {
    if err := hapusFile("catatan.txt"); err != nil {
        fmt.Println("error:", err)
        return
    }

    // coba hapus lagi — file sudah tidak ada
    if err := hapusFile("catatan.txt"); err != nil {
        fmt.Println("error:", err)
    }
}
file "catatan.txt" berhasil dihapus
error: file "catatan.txt" tidak ditemukan

Studi Kasus: Log Writer Sederhana

Menggabungkan semua operasi di atas menjadi sebuah logger sederhana yang mencatat aktivitas program ke file teks.

// filemanager.go
package main

import (
    "fmt"
    "os"
    "time"
)

const namaLog = "aktivitas.log"

func inisialisasiLog() error {
    _, err := os.Stat(namaLog)
    if os.IsNotExist(err) {
        f, err := os.Create(namaLog)
        if err != nil {
            return fmt.Errorf("gagal membuat file log: %w", err)
        }
        f.Close()
    }
    return nil
}

func catatLog(pesan string) error {
    f, err := os.OpenFile(namaLog, os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("gagal membuka log: %w", err)
    }
    defer f.Close()

    waktu := time.Now().Format("2006-01-02 15:04:05")
    baris := fmt.Sprintf("[%s] %s\n", waktu, pesan)

    _, err = f.WriteString(baris)
    if err != nil {
        return fmt.Errorf("gagal menulis log: %w", err)
    }

    return f.Sync()
}

func tampilkanLog() error {
    isi, err := os.ReadFile(namaLog)
    if err != nil {
        return fmt.Errorf("gagal membaca log: %w", err)
    }
    fmt.Println("=== Log Aktivitas ===")
    fmt.Print(string(isi))
    return nil
}

func bersihkanLog() error {
    return os.Remove(namaLog)
}

func main() {
    if err := inisialisasiLog(); err != nil {
        fmt.Println(err)
        return
    }

    aktivitas := []string{
        "program dimulai",
        "membaca konfigurasi",
        "koneksi database berhasil",
        "memproses 42 record",
        "program selesai",
    }

    for _, a := range aktivitas {
        if err := catatLog(a); err != nil {
            fmt.Println("error mencatat:", err)
            return
        }
    }

    if err := tampilkanLog(); err != nil {
        fmt.Println(err)
        return
    }

    if err := bersihkanLog(); err != nil {
        fmt.Println(err)
    }
}
=== Log Aktivitas ===
[2026-03-13 10:15:32] program dimulai
[2026-03-13 10:15:32] membaca konfigurasi
[2026-03-13 10:15:32] koneksi database berhasil
[2026-03-13 10:15:32] memproses 42 record
[2026-03-13 10:15:32] program selesai

Perhatikan penggunaan os.ReadFile() di tampilkanLog() — untuk kebutuhan membaca seluruh isi file kecil sekaligus, ini jauh lebih ringkas dibanding loop manual.

Latihan

Latihan 1 — Rotasi log: Modifikasi catatLog() agar memeriksa ukuran file sebelum menulis. Gunakan f.Stat().Size() untuk mendapatkan ukuran dalam byte. Jika ukuran sudah melebihi 1KB, rename file lama menjadi aktivitas.log.bak menggunakan os.Rename(), lalu buat file log baru.

Latihan 2 — Pencadangan file: Buat fungsi cadangkanFile(sumber, tujuan string) error yang membaca isi file sumber lalu menulisnya ke file tujuan. Gunakan os.ReadFile() untuk membaca dan os.WriteFile() untuk menulis. Tambahkan pengecekan agar tidak menimpa file tujuan jika sudah ada.

Latihan 3 — Integrasi dengan exec: Gabungkan os/exec dari Bab 43 dengan operasi file di bab ini. Buat program yang menjalankan git log --oneline -10, lalu menyimpan outputnya ke file git-history.txt. Jika file sudah ada dari run sebelumnya, tambahkan pemisah baris --- sebelum entri baru.


Operasi file adalah kapabilitas fundamental yang muncul di hampir setiap program nyata — dari menyimpan konfigurasi, mencatat log, sampai mengolah data. Go sengaja membuat API-nya konsisten: buka dengan os.Open atau os.OpenFile, tutup dengan defer f.Close(), dan tangani setiap error secara eksplisit. Pola yang sama berlaku untuk semua operasi I/O di Go, termasuk koneksi jaringan yang akan kita jelajahi di bab-bab selanjutnya.

Referensi

  1. 1Package os — Go Standard Library Documentation
  2. 2io.EOF — Go Standard Library Documentation
  3. 3Reading Files — Go by Example