Chapter 8 of 10
Setelah menyelesaikan bab ini, Anda mampu:
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.
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.
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.
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.
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.
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.
%w.Rename dapat gagal dan kehilangan sifat atomik.0o600 untuk file data pribadi dan 0o700 untuk direktori.defer untuk input; tangani error close pada output penting.CreatedAt time.Time dan amati representasi JSON-nya.json.Decoder dengan DisallowUnknownFields untuk memuat file.Backup(path string) yang menyalin data lama sebelum perubahan.Load untuk file tidak ada, kosong, JSON rusak, dan JSON valid.-file ke CLI kecil dan gunakan t.TempDir() dalam pengujian.Sync, Close, dan Rename mengurangi risiko korupsi saat menyimpan.