Configuration Management

Go memiliki beberapa cara untuk mengelola konfigurasi aplikasi, termasuk environment variables, file konfigurasi (JSON, YAML, TOML), dan command-line flags.

Contoh Masalah

Bagaimana cara:

  1. Mengelola konfigurasi
  2. Load dari berbagai sumber
  3. Validasi konfigurasi
  4. Hot reload

Penyelesaian

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "os"
    "path/filepath"
    "sync"
    "time"
)

// 1. Configuration structure
type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    User     string `json:"user"`
    Password string `json:"password"`
    Database string `json:"database"`
}

type ServerConfig struct {
    Host    string `json:"host"`
    Port    int    `json:"port"`
    Timeout int    `json:"timeout"`
}

type LogConfig struct {
    Level      string `json:"level"`
    File       string `json:"file"`
    MaxSize    int    `json:"max_size"`
    MaxBackups int    `json:"max_backups"`
}

type Config struct {
    Environment string         `json:"environment"`
    Debug       bool          `json:"debug"`
    Database    DatabaseConfig `json:"database"`
    Server      ServerConfig   `json:"server"`
    Log         LogConfig      `json:"log"`
}

// 2. Configuration manager
type ConfigManager struct {
    sync.RWMutex
    config     *Config
    configFile string
    logger     *log.Logger
}

// 3. Configuration validation
func (c *Config) Validate() error {
    if c.Environment == "" {
        return fmt.Errorf("environment must be set")
    }

    if c.Database.Host == "" {
        return fmt.Errorf("database host must be set")
    }

    if c.Database.Port <= 0 {
        return fmt.Errorf("invalid database port")
    }

    if c.Server.Port <= 0 {
        return fmt.Errorf("invalid server port")
    }

    return nil
}

// 4. Configuration string representation
func (c *Config) String() string {
    return fmt.Sprintf(
        "Environment: %s\n"+
            "Debug: %v\n"+
            "Database:\n"+
            "  Host: %s\n"+
            "  Port: %d\n"+
            "  User: %s\n"+
            "  Database: %s\n"+
            "Server:\n"+
            "  Host: %s\n"+
            "  Port: %d\n"+
            "  Timeout: %d\n"+
            "Log:\n"+
            "  Level: %s\n"+
            "  File: %s\n"+
            "  MaxSize: %d\n"+
            "  MaxBackups: %d\n",
        c.Environment,
        c.Debug,
        c.Database.Host,
        c.Database.Port,
        c.Database.User,
        c.Database.Database,
        c.Server.Host,
        c.Server.Port,
        c.Server.Timeout,
        c.Log.Level,
        c.Log.File,
        c.Log.MaxSize,
        c.Log.MaxBackups,
    )
}

// 5. Create new config manager
func NewConfigManager(configFile string) *ConfigManager {
    return &ConfigManager{
        configFile: configFile,
        logger:     log.New(os.Stdout, "[Config] ", log.Ltime),
    }
}

// 6. Load configuration
func (cm *ConfigManager) Load() error {
    cm.Lock()
    defer cm.Unlock()

    // Read config file
    data, err := os.ReadFile(cm.configFile)
    if err != nil {
        return fmt.Errorf("reading config file: %v", err)
    }

    // Parse JSON
    config := &Config{}
    if err := json.Unmarshal(data, config); err != nil {
        return fmt.Errorf("parsing config file: %v", err)
    }

    // Load environment variables
    cm.loadEnvVars(config)

    // Load command line flags
    cm.loadFlags(config)

    // Validate
    if err := config.Validate(); err != nil {
        return fmt.Errorf("validating config: %v", err)
    }

    cm.config = config
    return nil
}

// 7. Load environment variables
func (cm *ConfigManager) loadEnvVars(config *Config) {
    if env := os.Getenv("APP_ENV"); env != "" {
        config.Environment = env
    }

    if debug := os.Getenv("APP_DEBUG"); debug == "true" {
        config.Debug = true
    }

    if dbHost := os.Getenv("DB_HOST"); dbHost != "" {
        config.Database.Host = dbHost
    }

    // Add more environment variables as needed
}

// 8. Load command line flags
func (cm *ConfigManager) loadFlags(config *Config) {
    // Define flags
    env := flag.String("env", "", "Environment (development/production)")
    debug := flag.Bool("debug", false, "Enable debug mode")
    dbHost := flag.String("db-host", "", "Database host")
    serverPort := flag.Int("port", 0, "Server port")

    // Parse flags
    flag.Parse()

    // Apply flags if set
    if *env != "" {
        config.Environment = *env
    }
    if *debug {
        config.Debug = true
    }
    if *dbHost != "" {
        config.Database.Host = *dbHost
    }
    if *serverPort > 0 {
        config.Server.Port = *serverPort
    }
}

// 9. Get configuration
func (cm *ConfigManager) Get() *Config {
    cm.RLock()
    defer cm.RUnlock()
    return cm.config
}

// 10. Watch for config changes
func (cm *ConfigManager) Watch() {
    ticker := time.NewTicker(30 * time.Second)
    go func() {
        for range ticker.C {
            if err := cm.Load(); err != nil {
                cm.logger.Printf("Error reloading config: %v\n", err)
            } else {
                cm.logger.Println("Configuration reloaded")
            }
        }
    }()
}

func main() {
    // Create example config file
    exampleConfig := Config{
        Environment: "development",
        Debug:      true,
        Database: DatabaseConfig{
            Host:     "localhost",
            Port:     5432,
            User:     "user",
            Password: "password",
            Database: "myapp",
        },
        Server: ServerConfig{
            Host:    "localhost",
            Port:    8080,
            Timeout: 30,
        },
        Log: LogConfig{
            Level:      "info",
            File:       "app.log",
            MaxSize:    100,
            MaxBackups: 3,
        },
    }

    // Write example config
    configFile := "config.json"
    data, _ := json.MarshalIndent(exampleConfig, "", "    ")
    os.WriteFile(configFile, data, 0644)
    defer os.Remove(configFile)

    // Create config manager
    cm := NewConfigManager(configFile)

    // Load initial configuration
    if err := cm.Load(); err != nil {
        log.Fatalf("Error loading config: %v", err)
    }

    // Start watching for changes
    cm.Watch()

    // Print current config
    config := cm.Get()
    fmt.Println("Current configuration:")
    fmt.Println(config)

    // Example: Update config file
    time.Sleep(2 * time.Second)
    exampleConfig.Server.Port = 9090
    data, _ = json.MarshalIndent(exampleConfig, "", "    ")
    os.WriteFile(configFile, data, 0644)

    // Wait for config reload
    time.Sleep(2 * time.Second)
    config = cm.Get()
    fmt.Println("\nUpdated configuration:")
    fmt.Println(config)
}

Penjelasan Kode

  1. Config Sources

    • JSON file
    • Environment variables
    • Command line flags
  2. Features

    • Validation
    • Hot reload
    • Thread safety
  3. Best Practices

    • Centralized config
    • Type safety
    • Default values

Output

11:57:22 [Config] Configuration loaded
Current configuration:
Environment: development
Debug: true
Database:
  Host: localhost
  Port: 5432
  User: user
  Database: myapp
Server:
  Host: localhost
  Port: 8080
  Timeout: 30
Log:
  Level: info
  File: app.log
  MaxSize: 100
  MaxBackups: 3

11:57:24 [Config] Configuration reloaded
Updated configuration:
Environment: development
Debug: true
Database:
  Host: localhost
  Port: 5432
  User: user
  Database: myapp
Server:
  Host: localhost
  Port: 9090
  Timeout: 30
Log:
  Level: info
  File: app.log
  MaxSize: 100
  MaxBackups: 3

Tips

  • Gunakan strong typing
  • Validasi semua input
  • Sediakan defaults
  • Handle reload errors
  • Secure sensitive data