142. Pengenalan Configuration Management & Package spf13/viper
Memahami konsep 12-Factor App dalam memisahkan konfigurasi dari source code menggunakan library Viper.
Mengapa Membutuhkan Library Configuration Management?
Prinsip 12-Factor App mewajibkan konfigurasi (seperti credential database, port server, API key) dipisahkan dari source code aplikasi. Menulis ulang kode setiap kali pindah environment (Dev, Staging, Production) adalah praktek yang buruk.
- Mendukung file JSON, TOML, YAML, HCL, ENV, dan INI.
- Membaca nilai dari Environment Variables secara otomatis.
- Membaca dari Command-Line Flags (pflag).
- Live Reloading (hot reload) file konfigurasi saat aplikasi berjalan.
Instalasi Viper:
go get github.com/spf13/viper
Contoh program
package main
import (
"fmt"
)
type Config struct {
AppName string
Port int
Debug bool
}
func main() {
// Simulasi pembacaan konfigurasi sederhana
config := Config{
AppName: "Golang Microservice",
Port: 8080,
Debug: true,
}
fmt.Println("=== DEMO VIPER CONFIGURATION MANAGEMENT ===")
fmt.Printf("App Name : %s\n", config.AppName)
fmt.Printf("App Port : %d\n", config.Port)
fmt.Printf("Debug : %t\n", config.Debug)
}
143. Membaca File Konfigurasi (JSON, YAML, ENV file)
Mengkonfigurasi Viper untuk menemukan dan membaca file konfigurasi dalam berbagai format.
Menentukan Lokasi & Format File Konfigurasi
Viper fleksibel dalam membaca file. Kita perlu memberitahu Viper: nama file, tipe/format file, dan folder tempat file berada.
v := viper.New()
v.SetConfigName("config") // Nama file tanpa ekstensi (config.json / config.yaml)
v.SetConfigType("json") // Format file
v.AddConfigPath(".") // Cari di direktori saat ini
v.AddConfigPath("./config") // Cari di folder ./config
err := v.ReadInConfig() // Eksekusi pembacaan
if err != nil {
panic(fmt.Errorf("Fatal error config file: %w", err))
}
Contoh program
package main
import (
"fmt"
"strings"
)
// Simulasi in-memory parser file YAML/JSON
func MockReadConfig(fileContent string) map[string]string {
result := make(map[string]string)
lines := strings.Split(fileContent, "\n")
for _, line := range lines {
parts := strings.Split(line, "=")
if len(parts) == 2 {
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return result
}
func main() {
mockEnvFile := `APP_NAME=PaymentService
PORT=3000
DATABASE_HOST=localhost`
configData := MockReadConfig(mockEnvFile)
fmt.Println("=== HASIL BACA CONFIG FILE ===")
for k, v := range configData {
fmt.Printf("%s => %s\n", k, v)
}
}
144. Mengambil Nilai Konfigurasi (GetString, GetInt, GetBool, GetStringSlice)
Mengekstrak data dari Viper menggunakan tipe data yang aman (type-safe getters) dan memberikan nilai default.
Getter Methods pada Viper
Setelah file terbaca, kita bisa mengambil nilai berdasarkan Key menggunakan fungsi sesuai tipe datanya.
| Method | Return Type | Contoh Penggunaan |
|---|---|---|
GetString(key) |
string |
v.GetString("database.host") |
GetInt(key) |
int |
v.GetInt("server.port") |
GetBool(key) |
bool |
v.GetBool("app.debug") |
GetStringSlice(key) |
[]string |
v.GetStringSlice("cors.allowed_origins") |
v.SetDefault("server.port", 8080)
v.SetDefault("database.max_connection", 10)
Contoh program
package main
import (
"fmt"
)
type ConfigProvider struct {
data map[string]interface{}
}
func (c *ConfigProvider) GetString(key string) string {
if val, ok := c.data[key]; ok {
return val.(string)
}
return ""
}
func (c *ConfigProvider) GetInt(key string) int {
if val, ok := c.data[key]; ok {
return val.(int)
}
return 0
}
func main() {
provider := ConfigProvider{
data: map[string]interface{}{
"server.name": "E-Commerce Gateway",
"server.port": 9000,
},
}
fmt.Println("=== VIPER TYPE-SAFE GETTERS DEMO ===")
fmt.Println("Server Name:", provider.GetString("server.name"))
fmt.Println("Server Port:", provider.GetInt("server.port"))
}
145. Unmarshaling Config ke Struct (viper.Unmarshal)
Mengubah seluruh isi objek Viper menjadi Go Struct yang strongly-typed menggunakan struct tag mapstructure.
Mapping Otomatis ke Struct
Mengambil nilai satu-per-satu dengan `GetString` kurang efisien untuk proyek besar. Viper dapat memetakan seluruh hirarki konfigurasi ke Go Struct secara sekaligus menggunakan v.Unmarshal(&configStruct).
Viper menggunakan package mapstructure dibelakang layar. Gunakan tag mapstructure:"nama_key" pada struct field:
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
}
type AppConfig struct {
AppName string `mapstructure:"app_name"`
Database DatabaseConfig `mapstructure:"database"`
}
Contoh program
package main
import (
"encoding/json"
"fmt"
)
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
type AppConfig struct {
Name string `json:"name"`
Port int `json:"port"`
Database DatabaseConfig `json:"database"`
}
func main() {
rawJSON := `{
"name": "Inventory API",
"port": 8080,
"database": {
"host": "127.0.0.1",
"port": 3306
}
}`
var config AppConfig
err := json.Unmarshal([]byte(rawJSON), &config)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println("=== STRUCT UNMARSHAL CONFIG RESULT ===")
fmt.Printf("App Name : %s\n", config.Name)
fmt.Printf("DB Host : %s:%d\n", config.Database.Host, config.Database.Port)
}
146. Environment Variables Integration (AutomaticEnv, BindEnv, SetEnvPrefix)
Membaca dan menimpa konfigurasi file menggunakan Environment Variable dari OS secara dinamis.
Overriding Konfigurasi via Environment Variables
Di lingkungan produksi (seperti Docker atau Kubernetes), nilai konfigurasi rahasia dikirim via OS Environment Variables. Viper memiliki hierarki preseden di mana **Environment Variable akan menimpa (override) nilai dari File Config**.
v.AutomaticEnv(): Otomatis membaca ENV variable yang namanya cocok dengan key config.v.SetEnvPrefix("MYAPP"): Menambahkan prefix (misal `MYAPP_PORT`).v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")): Mengubah titik pada key nested menjadi underscore (misal `DATABASE.HOST` -> `MYAPP_DATABASE_HOST`).
Contoh program
package main
import (
"fmt"
"strings"
)
// Simulasi hierarki preseden Viper (ENV override File Config)
func ResolveConfig(fileVal, envVal string) string {
if strings.TrimSpace(envVal) != "" {
return envVal + " (From OS Environment)"
}
return fileVal + " (From File Config)"
}
func main() {
fileHost := "localhost"
envHost := "production-db.internal" // Diset di OS
resolved := ResolveConfig(fileHost, envHost)
fmt.Println("=== RESOLVED DB HOST ===")
fmt.Println("Host Used:", resolved)
}
147. Dynamic Config Watching & Hot Reload (viper.WatchConfig)
Menerapkan fitur Live Reload untuk memperbarui nilai konfigurasi secara otomatis tanpa perlu restart server.
Fitur Hot Reload File Konfigurasi
Viper mampu memantau perubahan file di sistem (menggunakan library `fsnotify`). Saat file config diubah dan disimpan, Viper akan otomatis membaca ulang tanpa menghentikan aplikasi (*zero downtime*).
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Re-unmarshal atau perbarui variabel global di sini
v.Unmarshal(&globalConfig)
})
Contoh program
package main
import (
"fmt"
"sync"
)
type DynamicConfig struct {
mu sync.RWMutex
FeatureFlag bool
}
func (c *DynamicConfig) UpdateFlag(val bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.FeatureFlag = val
fmt.Println("[EVENT] Config updated! FeatureFlag is now:", val)
}
func (c *DynamicConfig) IsEnabled() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.FeatureFlag
}
func main() {
cfg := &DynamicConfig{FeatureFlag: false}
fmt.Println("Initial Feature Flag:", cfg.IsEnabled())
// Simulasi Hot Reload dari WatchConfig
cfg.UpdateFlag(true)
fmt.Println("After Hot Reload Feature Flag:", cfg.IsEnabled())
}
148. Command Line Flags Integration dengan Cobra/Pflag
Mengintegrasikan argument CLI (flags) dengan Viper sehingga parameter terminal dapat meng-override file config.
Integrasi CLI Flag & Viper
Saat membuat aplikasi CLI atau background worker di Go, pengguna sering kali menyuplai flag saat menjalankan perintah terminal (misal: ./app --port=9090).
pflag.Int("port", 8080, "Port untuk HTTP Server")
pflag.Parse()
v.BindPFlags(pflag.CommandLine)
// Nilai --port dari terminal akan otomatis meng-override port dari file config!
Contoh program
package main
import (
"fmt"
)
func ParseCLIArgs(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if len(arg) > 2 && arg[:2] == "--" {
parts := strings.Split(arg[2:], "=")
if len(parts) == 2 {
flags[parts[0]] = parts[1]
}
}
}
return flags
}
func main() {
mockCLI := []string{"./main", "--port=9090", "--env=staging"}
flags := ParseCLIArgs(mockCLI)
fmt.Println("=== PARSED CLI FLAGS ===")
for k, v := range flags {
fmt.Printf("Flag '--%s' => %s\n", k, v)
}
}
149. Studi Kasus: App Config Provider Centralized Singleton
Mendesain arsitektur pemuatan konfigurasi terpusat (Singleton Provider) yang rapi, aman, dan siap dipakai di seluruh layer aplikasi.
Pattern Singleton Config Provider
Memuat konfigurasi berulang kali di setiap file adalah pemborosan. Pendekatan terbaik adalah membuat package `config` yang memuat seluruh konfigurasi sekali saat *startup* (menggunakan `sync.Once`), lalu membagikan pointer `*AppConfig` tersebut.
├── config/ │ ├── config.go # Singleton loader (Viper unmarshal) │ └── config.json # Default config file ├── main.go # Inisialisasi config.LoadConfig()
Contoh program
package main
import (
"fmt"
"sync"
)
type Config struct {
AppName string
Port int
}
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("[INIT] Loading configuration singleton via Viper...")
instance = &Config{
AppName: "Enterprise Backend API",
Port: 8080,
}
})
return instance
}
func main() {
fmt.Println("=== SINGLETON CONFIG PROVIDER DEMO ===")
cfg1 := GetConfig()
cfg2 := GetConfig()
fmt.Println("Cfg1 AppName:", cfg1.AppName)
fmt.Println("Apakah Pointer Cfg1 dan Cfg2 sama?", cfg1 == cfg2)
}