CLI yang matang jarang menerima seluruh pengaturannya lewat flag saja; pengguna akan lelah mengetik --host dan --port pada setiap pemanggilan. Solusinya adalah berkas konfigurasi tetap yang menyimpan nilai bawaan, sementara flag tetap tersedia untuk penyesuaian sesaat. Pertanyaannya lalu: bila satu pengaturan muncul di beberapa tempat sekaligus, nilai mana yang menang? Pada chapter ini kita akan menerapkan urutan prioritas yang sudah menjadi konvensi — flag mengalahkan variabel lingkungan, variabel lingkungan mengalahkan berkas, dan berkas mengalahkan nilai default — memakai encoding/json dari standard library, lalu melihat format YAML lewat library gopkg.in/yaml.v3.

Urutan Prioritas Konfigurasi #

PrioritasSumberPeran
1 (tertinggi)Flag baris perintahPenyesuaian sekali jalan
2Variabel lingkunganPengaturan per sesi/lingkungan
3Berkas konfigurasiPengaturan tetap pengguna
4 (terendah)Nilai default di kodeJaring pengaman terakhir

Urutan ini membuat perilaku program mudah ditebak: berkas menjadi rumah nilai tetap, variabel lingkungan dipakai di server atau container, dan flag dipakai saat menguji satu nilai berbeda. Program membaca sumber mulai dari prioritas terendah, lalu nilai dari sumber berikutnya menimpa (overlay) nilai sebelumnya.

Membaca Berkas JSON dan Menggabungkannya #

Berkas config-demo.go di bawah menerapkan keempat lapis tersebut. Agar demo mandiri dan deterministik, program menulis sendiri berkas config.json ke direktori sementara, membacanya kembali, lalu menimpa nilai default secara bertingkat:

go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
)

// Config menampung seluruh konfigurasi aplikasi contoh.
type Config struct {
	Host   string `json:"host"`
	Port   int    `json:"port"`
	Debug  bool   `json:"debug"`
	Prefix string `json:"prefix"`
}

func main() {
	port := flag.Int("port", 0, "nomor port (0 berarti tidak diisi)")
	flag.Parse()

	// Lapis 1: nilai default.
	cfg := Config{Host: "localhost", Port: 8080, Debug: false, Prefix: "app"}
	fmt.Printf("default : host=%s port=%d debug=%v\n", cfg.Host, cfg.Port, cfg.Debug)

	// Lapis 2: nilai dari berkas konfigurasi JSON menimpa default.
	berkas := tulisBerkasDemo()
	data, err := os.ReadFile(berkas)
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membaca berkas:", err)
		os.Exit(1)
	}
	fmt.Println("isi berkas:")
	fmt.Println(string(data))

	var dariFile Config
	if err := json.Unmarshal(data, &dariFile); err != nil {
		fmt.Fprintln(os.Stderr, "gagal mengurai JSON:", err)
		os.Exit(1)
	}
	// Overlay: timpa hanya field yang terisi di berkas.
	if dariFile.Host != "" {
		cfg.Host = dariFile.Host
	}
	if dariFile.Port != 0 {
		cfg.Port = dariFile.Port
	}
	if dariFile.Debug {
		cfg.Debug = dariFile.Debug
	}
	fmt.Printf("berkas  : host=%s port=%d debug=%v\n", cfg.Host, cfg.Port, cfg.Debug)

	// Lapis 3: variabel lingkungan menimpa berkas.
	envPort := os.Getenv("APP_PORT")
	fmt.Println("env     : APP_PORT=" + envPort)
	if p, err := strconv.Atoi(envPort); err == nil && p != 0 {
		cfg.Port = p
	}

	// Lapis 4: flag paling tinggi prioritasnya.
	if *port != 0 {
		fmt.Printf("flag    : -port=%d\n", *port)
		cfg.Port = *port
	} else {
		fmt.Println("flag    : (tidak diisi)")
	}

	fmt.Printf("aktif   : host=%s port=%d debug=%v prefix=%s\n", cfg.Host, cfg.Port, cfg.Debug, cfg.Prefix)
}

// tulisBerkasDemo membuat berkas config.json di direktori sementara,
// lalu mengembalikan lokasinya untuk dibaca kembali.
func tulisBerkasDemo() string {
	dir, err := os.MkdirTemp("", "config-demo")
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membuat direktori sementara:", err)
		os.Exit(1)
	}
	path := filepath.Join(dir, "config.json")

	isi, err := json.MarshalIndent(Config{Host: "db.example.com", Port: 5432, Debug: true}, "", "  ")
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membuat JSON:", err)
		os.Exit(1)
	}
	if err := os.WriteFile(path, isi, 0o600); err != nil {
		fmt.Fprintln(os.Stderr, "gagal menulis berkas:", err)
		os.Exit(1)
	}
	return path
}

Jalankan program dengan variabel lingkungan dan flag tetap agar seluruh lapis terlihat:

bash
APP_PORT=9090 go run config-demo.go -port 7070

Output yang Dihasilkan:

text
default : host=localhost port=8080 debug=false
isi berkas:
{
  "host": "db.example.com",
  "port": 5432,
  "debug": true,
  "prefix": ""
}
berkas  : host=db.example.com port=5432 debug=true
env     : APP_PORT=9090
flag    : -port=7070
aktif   : host=db.example.com port=7070 debug=true prefix=app

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-18 (type Config struct): struct konfigurasi dengan struct tag json:"host" yang memetakan field ke kunci JSON; json.MarshalIndent dan json.Unmarshal sama-sama membaca pemetaan ini.
  2. Baris 25-26 (nilai default): struct diisi nilai default lalu dicetak sebagai lapis pertama.
  3. Baris 29-36 (os.ReadFile): membaca seluruh isi berkas sebagai []byte, lalu mencetaknya mentah agar isi berkas terlihat pada output.
  4. Baris 38-52 (json.Unmarshal dan overlay): isi berkas diurai ke struct kedua dariFile, lalu setiap field yang terisi menimpa cfg; field kosong dibiarkan agar nilai default tidak hilang.
  5. Baris 56-60 (os.Getenv dan strconv.Atoi): variabel lingkungan APP_PORT dibaca sebagai teks lalu dikonversi; konversi yang gagal berarti variabel tidak diisi sehingga nilai berkas bertahan.
  6. Baris 63-68 (flag tertinggi): nilai flag 0 dipakai sebagai penanda “tidak diisi” sehingga flag yang absen tidak menimpa hasil lapis sebelumnya.
  7. Baris 75-93 (tulisBerkasDemo): membuat direktori sementara dengan os.MkdirTemp, menulis config.json berisi konfigurasi contoh, lalu mengembalikan lokasinya.

Bila flag tidak diisi, nilai APP_PORT yang menang atas berkas:

bash
APP_PORT=9090 go run config-demo.go

Output yang Dihasilkan:

text
default : host=localhost port=8080 debug=false
isi berkas:
{
  "host": "db.example.com",
  "port": 5432,
  "debug": true,
  "prefix": ""
}
berkas  : host=db.example.com port=5432 debug=true
env     : APP_PORT=9090
flag    : (tidak diisi)
aktif   : host=db.example.com port=9090 debug=true prefix=app

Dan bila variabel lingkungan juga absen, nilai dari berkas yang bertahan sampai akhir:

bash
go run config-demo.go

Output yang Dihasilkan:

text
default : host=localhost port=8080 debug=false
isi berkas:
{
  "host": "db.example.com",
  "port": 5432,
  "debug": true,
  "prefix": ""
}
berkas  : host=db.example.com port=5432 debug=true
env     : APP_PORT=
flag    : (tidak diisi)
aktif   : host=db.example.com port=5432 debug=true prefix=app

Tanpa satu pun sumber eksternal, nilai default yang tampil — inilah manfaat lapis berurutan: setiap lapis yang kosong cukup dilewati, dan program tidak pernah kehabisan nilai.

Lokasi Berkas Konvensi #

Program sungguhan biasanya mencari berkas konfigurasi di lokasi yang sudah disepakati. Konvensi XDG di Linux menempatkannya di ~/.config/<nama-app>/config.json, sementara macOS memakai ~/Library/Application Support/<nama-app> dan Windows memakai folder %APPDATA%. Fungsi os.UserConfigDir() mengembalikan lokasi dasar yang sesuai untuk sistem operasi tempat program berjalan, sehingga kita cukup menggabungkannya dengan filepath.Join untuk membangun jalur lengkap. Selain itu, sediakan flag --config agar pengguna dapat menunjuk berkas lain.

YAML dengan gopkg.in/yaml.v3 #

JSON bersifat bawaan Go, tetapi konfigurasi manusia sering ditulis dalam YAML yang lebih ringkas. Library gopkg.in/yaml.v3 membacanya dengan cara yang sama persis: struct dengan struct tag, lalu Unmarshal. Berikut contoh yang dijalankan pada module terpisah di luar bundle halaman karena memakai dependency pihak ketiga:

go
type Server struct {
        Host     string `yaml:"host"`
        Port     int    `yaml:"port"`
        Username string `yaml:"username"`
}

type Config struct {
        Debug  bool   `yaml:"debug"`
        Prefix string `yaml:"prefix"`
        Server Server `yaml:"server"`
}

isi := `
debug: true
prefix: app
server:
  host: db.example.com
  port: 5432
  username: budi
`

var cfg Config
err := yaml.Unmarshal([]byte(isi), &cfg)

Output yang Dihasilkan:

text
debug: true
prefix: app
server.host: db.example.com
server.port: 5432
server.username: budi

Struct bersarang memetakan blok server: pada YAML ke struct Server, masing-masing dengan tag yaml. Framework seperti Cobra berintegrasi mulus dengan library Viper yang membaca JSON, YAML, dan variabel lingkungan sekaligus dengan urutan prioritas seperti tabel di awal chapter.

Pemilihan format akhirnya soal selera tim. JSON unggul dalam tooling bawaan (encoding/json tanpa dependency, editor mana pun memvalidasinya) dan cocok untuk berkas yang ditulis mesin. YAML lebih ringkas untuk ditulis manusia — tanpa koma dan tanda kutip — tetapi membutuhkan dependency pihak ketiga dan sensitif terhadap indentasi: satu spasi yang melenceng mengubah struktur tanpa pesan galat yang jelas. Untuk konfigurasi bersifat rahasia seperti token, kedua format sama-sama tidak pantas; nilai sensitif sebaiknya datang dari variabel lingkungan atau secret manager, bukan berkas yang mudah terbaca.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

config-demo.go

Unduh berkas
go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
)

// Config menampung seluruh konfigurasi aplikasi contoh.
type Config struct {
	Host   string `json:"host"`
	Port   int    `json:"port"`
	Debug  bool   `json:"debug"`
	Prefix string `json:"prefix"`
}

func main() {
	port := flag.Int("port", 0, "nomor port (0 berarti tidak diisi)")
	flag.Parse()

	// Lapis 1: nilai default.
	cfg := Config{Host: "localhost", Port: 8080, Debug: false, Prefix: "app"}
	fmt.Printf("default : host=%s port=%d debug=%v\n", cfg.Host, cfg.Port, cfg.Debug)

	// Lapis 2: nilai dari berkas konfigurasi JSON menimpa default.
	berkas := tulisBerkasDemo()
	data, err := os.ReadFile(berkas)
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membaca berkas:", err)
		os.Exit(1)
	}
	fmt.Println("isi berkas:")
	fmt.Println(string(data))

	var dariFile Config
	if err := json.Unmarshal(data, &dariFile); err != nil {
		fmt.Fprintln(os.Stderr, "gagal mengurai JSON:", err)
		os.Exit(1)
	}
	// Overlay: timpa hanya field yang terisi di berkas.
	if dariFile.Host != "" {
		cfg.Host = dariFile.Host
	}
	if dariFile.Port != 0 {
		cfg.Port = dariFile.Port
	}
	if dariFile.Debug {
		cfg.Debug = dariFile.Debug
	}
	fmt.Printf("berkas  : host=%s port=%d debug=%v\n", cfg.Host, cfg.Port, cfg.Debug)

	// Lapis 3: variabel lingkungan menimpa berkas.
	envPort := os.Getenv("APP_PORT")
	fmt.Println("env     : APP_PORT=" + envPort)
	if p, err := strconv.Atoi(envPort); err == nil && p != 0 {
		cfg.Port = p
	}

	// Lapis 4: flag paling tinggi prioritasnya.
	if *port != 0 {
		fmt.Printf("flag    : -port=%d\n", *port)
		cfg.Port = *port
	} else {
		fmt.Println("flag    : (tidak diisi)")
	}

	fmt.Printf("aktif   : host=%s port=%d debug=%v prefix=%s\n", cfg.Host, cfg.Port, cfg.Debug, cfg.Prefix)
}

// tulisBerkasDemo membuat berkas config.json di direktori sementara,
// lalu mengembalikan lokasinya untuk dibaca kembali.
func tulisBerkasDemo() string {
	dir, err := os.MkdirTemp("", "config-demo")
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membuat direktori sementara:", err)
		os.Exit(1)
	}
	path := filepath.Join(dir, "config.json")

	isi, err := json.MarshalIndent(Config{Host: "db.example.com", Port: 5432, Debug: true}, "", "  ")
	if err != nil {
		fmt.Fprintln(os.Stderr, "gagal membuat JSON:", err)
		os.Exit(1)
	}
	if err := os.WriteFile(path, isi, 0o600); err != nil {
		fmt.Fprintln(os.Stderr, "gagal menulis berkas:", err)
		os.Exit(1)
	}
	return path
}