Kodeloka
  1. Kodeloka
  2. Kelas Golang
  3. Materi

Domain 12 · 8 bab

Golang Viper Configuration

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.

💡 Fitur Utama spf13/viper:
  • 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)
}

Praktik dan kuis bab ini di kelas interaktif →

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.

📌 Kode Konfigurasi Viper:
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)
	}
}

Praktik dan kuis bab ini di kelas interaktif →

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")
💡 Setting Default Value:
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"))
}

Praktik dan kuis bab ini di kelas interaktif →

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).

📌 Struct Tag `mapstructure`:

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)
}

Praktik dan kuis bab ini di kelas interaktif →

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**.

💡 Fungsi Penting ENV di Viper:
  • 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)
}

Praktik dan kuis bab ini di kelas interaktif →

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*).

📌 Mengaktifkan WatchConfig:
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())
}

Praktik dan kuis bab ini di kelas interaktif →

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).

💡 Membind Pflag ke Viper:
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)
	}
}

Praktik dan kuis bab ini di kelas interaktif →

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.

📌 Struktur Folder Rekomendasi:
├── 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)
}

Praktik dan kuis bab ini di kelas interaktif →