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 #
| Prioritas | Sumber | Peran |
|---|---|---|
| 1 (tertinggi) | Flag baris perintah | Penyesuaian sekali jalan |
| 2 | Variabel lingkungan | Pengaturan per sesi/lingkungan |
| 3 | Berkas konfigurasi | Pengaturan tetap pengguna |
| 4 (terendah) | Nilai default di kode | Jaring 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:
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:
APP_PORT=9090 go run config-demo.go -port 7070Output yang Dihasilkan:
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=appMari kita telaah baris demi baris program di atas:
- Baris 13-18 (
type Config struct): struct konfigurasi dengan struct tagjson:"host"yang memetakan field ke kunci JSON;json.MarshalIndentdanjson.Unmarshalsama-sama membaca pemetaan ini. - Baris 25-26 (nilai default): struct diisi nilai default lalu dicetak sebagai lapis pertama.
- Baris 29-36 (
os.ReadFile): membaca seluruh isi berkas sebagai[]byte, lalu mencetaknya mentah agar isi berkas terlihat pada output. - Baris 38-52 (
json.Unmarshaldan overlay): isi berkas diurai ke struct keduadariFile, lalu setiap field yang terisi menimpacfg; field kosong dibiarkan agar nilai default tidak hilang. - Baris 56-60 (
os.Getenvdanstrconv.Atoi): variabel lingkunganAPP_PORTdibaca sebagai teks lalu dikonversi; konversi yang gagal berarti variabel tidak diisi sehingga nilai berkas bertahan. - Baris 63-68 (flag tertinggi): nilai flag
0dipakai sebagai penanda “tidak diisi” sehingga flag yang absen tidak menimpa hasil lapis sebelumnya. - Baris 75-93 (
tulisBerkasDemo): membuat direktori sementara denganos.MkdirTemp, menulisconfig.jsonberisi konfigurasi contoh, lalu mengembalikan lokasinya.
Bila flag tidak diisi, nilai APP_PORT yang menang atas berkas:
APP_PORT=9090 go run config-demo.goOutput yang Dihasilkan:
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=appDan bila variabel lingkungan juga absen, nilai dari berkas yang bertahan sampai akhir:
go run config-demo.goOutput yang Dihasilkan:
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=appTanpa 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:
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:
debug: true
prefix: app
server.host: db.example.com
server.port: 5432
server.username: budiStruct 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 #
- Dokumentasi encoding/json
—
Marshal,Unmarshal, dan struct tag. - Dokumentasi package os
—
ReadFile,UserConfigDir, danGetenv.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
config-demo.go
Unduh berkaspackage 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
}