Bab 8 dari 10
File I/O, JSON, dan Persistensi CLI
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
- membaca dan menulis file dengan pustaka standar;
- mengubah struct menjadi JSON dan sebaliknya;
- membedakan file tidak ada dari kegagalan I/O lain;
- menyimpan data CLI secara atomik;
- menangani path, permission, dan error dengan aman.
1. Membaca dan Menulis File
Untuk file kecil yang harus dimuat seluruhnya, gunakan os.ReadFile dan os.WriteFile.
package main
import (
"fmt"
"log"
"os"
)
func main() {
const path = "message.txt"
if err := os.WriteFile(path, []byte("Halo, Go!\n"), 0o600); err != nil {
log.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Print(string(data))
}
Mode 0o600 memberi akses baca-tulis kepada pemilik. Permission aktual dapat dipersempit oleh umask. Untuk data pengguna yang mungkin sensitif, jangan memakai mode yang dapat dibaca semua pengguna.
Untuk data besar atau streaming, gunakan os.Open, bufio.Scanner, bufio.Reader, atau io.Copy. File yang dibuka harus ditutup.
f, err := os.Open("input.txt")
if err != nil {
return err
}
defer f.Close()
Pada file output, error Close dapat penting karena flush terakhir bisa gagal. Tangani secara eksplisit jika kehilangan data berisiko.
2. JSON dan Struct Tags
Package encoding/json memetakan field struct yang diekspor. Tag mengatur nama key.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
func main() {
tasks := []Task{{ID: 1, Title: "Belajar JSON", Done: false}}
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Untuk membaca JSON yang tidak dipercaya, decoder dapat menolak field asing:
decoder := json.NewDecoder(r)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return fmt.Errorf("decode JSON: %w", err)
}
Marshal hanya gagal untuk nilai yang tidak didukung, seperti channel, function, atau siklus pointer. Tetap tangani error.
3. Memuat Data dengan Semantik yang Jelas
File belum ada dapat berarti aplikasi baru, bukan kegagalan.
func load(path string) ([]Task, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return []Task{}, nil
}
if err != nil {
return nil, fmt.Errorf("baca %q: %w", path, err)
}
if len(data) == 0 {
return []Task{}, nil
}
var tasks []Task
if err := json.Unmarshal(data, &tasks); err != nil {
return nil, fmt.Errorf("decode %q: %w", path, err)
}
return tasks, nil
}
Jangan memperlakukan permission denied atau kerusakan disk sebagai “data kosong”; tindakan tersebut menyembunyikan masalah dan berpotensi menimpa data.
4. Penulisan Atomik
os.WriteFile langsung ke file tujuan dapat meninggalkan file terpotong bila proses berhenti. Pola yang lebih aman adalah menulis file sementara pada direktori yang sama, menyinkronkan, menutup, lalu mengganti nama.
Program lengkap berikut menyediakan penyimpanan JSON sederhana:
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
func Load(path string) ([]Task, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return []Task{}, nil
}
if err != nil {
return nil, fmt.Errorf("baca %q: %w", path, err)
}
if len(data) == 0 {
return []Task{}, nil
}
var tasks []Task
if err := json.Unmarshal(data, &tasks); err != nil {
return nil, fmt.Errorf("decode %q: %w", path, err)
}
return tasks, nil
}
func Save(path string, tasks []Task) (err error) {
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return fmt.Errorf("encode tugas: %w", err)
}
data = append(data, '\n')
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("buat direktori %q: %w", dir, err)
}
tmp, err := os.CreateTemp(dir, ".tasks-*.tmp")
if err != nil {
return fmt.Errorf("buat file sementara: %w", err)
}
tmpName := tmp.Name()
defer func() {
tmp.Close()
os.Remove(tmpName)
}()
if err := tmp.Chmod(0o600); err != nil {
return fmt.Errorf("atur permission: %w", err)
}
if _, err := tmp.Write(data); err != nil {
return fmt.Errorf("tulis file sementara: %w", err)
}
if err := tmp.Sync(); err != nil {
return fmt.Errorf("sinkronkan file sementara: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("tutup file sementara: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("ganti %q: %w", path, err)
}
return nil
}
func main() {
path := filepath.Join(os.TempDir(), "taskcli-example", "tasks.json")
tasks := []Task{{ID: 1, Title: "Simpan atomik"}}
if err := Save(path, tasks); err != nil {
panic(err)
}
loaded, err := Load(path)
if err != nil {
panic(err)
}
fmt.Println(loaded[0].Title)
}
Rename atomik secara umum jika sumber dan tujuan berada pada filesystem yang sama; karena itu file sementara dibuat di direktori tujuan. Jaminan detail tetap bergantung pada sistem operasi dan filesystem. Untuk ketahanan terhadap kehilangan daya yang lebih ketat, sinkronisasi direktori setelah rename dapat diperlukan.
5. Menentukan Lokasi Data CLI
Hindari menulis data tetap di current working directory karena direktori tersebut berubah sesuai cara program dijalankan. Gunakan direktori konfigurasi pengguna:
func defaultPath() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("tentukan direktori konfigurasi: %w", err)
}
return filepath.Join(dir, "taskcli", "tasks.json"), nil
}
Izinkan flag seperti -file agar pengujian dan otomasi dapat memilih lokasi lain. Jangan membangun path dengan penggabungan string; gunakan filepath.Join.
6. Batas Persistensi JSON
JSON sesuai untuk CLI pengguna tunggal dengan data kecil. Ia bukan solusi ideal untuk banyak proses yang menulis bersamaan, query kompleks, atau dataset besar. Penulisan atomik mencegah file setengah jadi, tetapi tidak mencegah lost update antara dua proses. Pada kebutuhan tersebut, tambahkan locking yang sesuai platform atau beralih ke database transaksional.
Kesalahan Umum
- Mengabaikan error I/O atau JSON. Selalu beri konteks dengan
%w. - Menimpa file ketika decode gagal. Hentikan operasi dan laporkan kerusakan data.
- Membuat temp file di filesystem lain.
Renamedapat gagal dan kehilangan sifat atomik. - Permission terlalu longgar. Gunakan
0o600untuk file data pribadi dan0o700untuk direktori. - Lupa menutup file. Gunakan
deferuntuk input; tangani error close pada output penting. - Mengandalkan current working directory. Gunakan direktori pengguna atau flag eksplisit.
- Menganggap JSON sebagai database multiwriter. Tambahkan koordinasi atau gunakan penyimpanan transaksional.
Latihan
- Tambahkan field
CreatedAt time.Timedan amati representasi JSON-nya. - Gunakan
json.DecoderdenganDisallowUnknownFieldsuntuk memuat file. - Tulis fungsi
Backup(path string)yang menyalin data lama sebelum perubahan. - Uji
Loaduntuk file tidak ada, kosong, JSON rusak, dan JSON valid. - Tambahkan flag
-fileke CLI kecil dan gunakant.TempDir()dalam pengujian.
Ringkasan
- Pilih API file sesuai ukuran dan pola akses data.
- Struct tag mengontrol format JSON; semua kegagalan encode/decode harus ditangani.
- Bedakan file tidak ada dari error I/O lain.
- Temp-file,
Sync,Close, danRenamemengurangi risiko korupsi saat menyimpan. - JSON cocok untuk persistensi sederhana, bukan konkurensi penulis tanpa koordinasi.