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.
Menulis ke File
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 Flag | Efek |
|---|---|
O_WRONLY | O_CREATE | O_TRUNC | Buat atau timpa file |
O_WRONLY | O_APPEND | Tambah di akhir file yang ada |
O_RDWR | Baca dan tulis |
O_RDONLY | Hanya 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.