1. go
  2. /best practices
  3. /configuration

Configuration Management Best Practices in Go

Effective configuration management is crucial for building flexible and maintainable Go applications. This guide covers best practices for handling configuration in your Go applications.

Configuration Sources

1. Environment Variables

Environment variables are the preferred way to configure applications:

package config

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

type Config struct {
    Server struct {
        Host string
        Port int
    }
    Database struct {
        URL      string
        MaxConns int
        Timeout  time.Duration
    }
    Features struct {
        EnableCache bool
        CacheSize  int
    }
}

func LoadFromEnv() (*Config, error) {
    cfg := &Config{}
    
    // Server configuration
    cfg.Server.Host = os.Getenv("SERVER_HOST")
    if cfg.Server.Host == "" {
        cfg.Server.Host = "localhost" // default
    }
    
    port, err := strconv.Atoi(os.Getenv("SERVER_PORT"))
    if err == nil {
        cfg.Server.Port = port
    } else {
        cfg.Server.Port = 8080 // default
    }
    
    // Database configuration
    cfg.Database.URL = os.Getenv("DATABASE_URL")
    if cfg.Database.URL == "" {
        return nil, fmt.Errorf("DATABASE_URL is required")
    }
    
    return cfg, nil
}

2. Configuration Files

Using configuration files with viper:

package config

import (
    "github.com/spf13/viper"
)

type Config struct {
    Server struct {
        Host string `mapstructure:"host"`
        Port int    `mapstructure:"port"`
    } `mapstructure:"server"`
    Database struct {
        URL      string        `mapstructure:"url"`
        MaxConns int           `mapstructure:"max_connections"`
        Timeout  time.Duration `mapstructure:"timeout"`
    } `mapstructure:"database"`
}

func LoadConfig(path string) (*Config, error) {
    viper.SetConfigFile(path)
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("failed to unmarshal config: %w", err)
    }
    
    return &cfg, nil
}

Example configuration file (config.yaml):

server:
  host: localhost
  port: 8080

database:
  url: postgres://localhost:5432/myapp
  max_connections: 100
  timeout: 30s

features:
  enable_cache: true
  cache_size: 1000

Best Practices

1. Configuration Validation

Validate configuration values:

type Config struct {
    Server ServerConfig
    Database DatabaseConfig
}

type ServerConfig struct {
    Port int `validate:"required,min=1024,max=65535"`
    Host string `validate:"required"`
}

type DatabaseConfig struct {
    URL      string        `validate:"required,url"`
    MaxConns int           `validate:"required,min=1,max=1000"`
    Timeout  time.Duration `validate:"required,min=1s,max=60s"`
}

func (c *Config) Validate() error {
    validate := validator.New()
    
    if err := validate.Struct(c); err != nil {
        return fmt.Errorf("invalid configuration: %w", err)
    }
    
    return nil
}

2. Secrets Management

Handle sensitive configuration securely:

type Secrets struct {
    DatabasePassword string
    APIKey          string
    JWTSecret       string
}

func LoadSecrets() (*Secrets, error) {
    // Load from environment
    secrets := &Secrets{
        DatabasePassword: os.Getenv("DATABASE_PASSWORD"),
        APIKey:          os.Getenv("API_KEY"),
        JWTSecret:       os.Getenv("JWT_SECRET"),
    }
    
    // Validate required secrets
    if secrets.DatabasePassword == "" {
        return nil, fmt.Errorf("DATABASE_PASSWORD is required")
    }
    
    // Mask secrets in logs
    log.Info().
        Str("database_password", "***").
        Str("api_key", "***").
        Msg("secrets loaded")
        
    return secrets, nil
}

3. Configuration Reloading

Implement dynamic configuration reloading:

type ConfigWatcher struct {
    config     *Config
    configPath string
    onChange   func(*Config)
    done       chan struct{}
}

func NewConfigWatcher(path string, onChange func(*Config)) *ConfigWatcher {
    return &ConfigWatcher{
        configPath: path,
        onChange:   onChange,
        done:      make(chan struct{}),
    }
}

func (w *ConfigWatcher) Watch() error {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        return err
    }
    defer watcher.Close()
    
    go func() {
        for {
            select {
            case event := <-watcher.Events:
                if event.Op&fsnotify.Write == fsnotify.Write {
                    if cfg, err := LoadConfig(w.configPath); err == nil {
                        w.config = cfg
                        w.onChange(cfg)
                    }
                }
            case <-w.done:
                return
            }
        }
    }()
    
    return watcher.Add(w.configPath)
}

Common Patterns

1. Feature Flags

Implement feature flags:

type Features struct {
    EnableNewUI     bool `mapstructure:"enable_new_ui"`
    EnableBetaAPI   bool `mapstructure:"enable_beta_api"`
    MaxConcurrency  int  `mapstructure:"max_concurrency"`
}

type FeatureManager struct {
    features *Features
    mu       sync.RWMutex
}

func (fm *FeatureManager) IsEnabled(feature string) bool {
    fm.mu.RLock()
    defer fm.mu.RUnlock()
    
    switch feature {
    case "new_ui":
        return fm.features.EnableNewUI
    case "beta_api":
        return fm.features.EnableBetaAPI
    default:
        return false
    }
}

2. Configuration Providers

Abstract configuration sources:

type ConfigProvider interface {
    Get(key string) (string, error)
    Set(key string, value string) error
    Watch(key string, callback func(string)) error
}

type EnvProvider struct{}

func (p *EnvProvider) Get(key string) (string, error) {
    value := os.Getenv(key)
    if value == "" {
        return "", fmt.Errorf("environment variable %s not set", key)
    }
    return value, nil
}

type ConsulProvider struct {
    client *consul.Client
}

func (p *ConsulProvider) Get(key string) (string, error) {
    kv, _, err := p.client.KV().Get(key, nil)
    if err != nil {
        return "", err
    }
    return string(kv.Value), nil
}

Testing

1. Configuration Testing

Test configuration loading and validation:

func TestLoadConfig(t *testing.T) {
    tests := []struct {
        name    string
        envVars map[string]string
        wantErr bool
    }{
        {
            name: "valid configuration",
            envVars: map[string]string{
                "SERVER_HOST": "localhost",
                "SERVER_PORT": "8080",
                "DATABASE_URL": "postgres://localhost:5432/test",
            },
            wantErr: false,
        },
        {
            name: "missing required field",
            envVars: map[string]string{
                "SERVER_HOST": "localhost",
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Set environment variables
            for k, v := range tt.envVars {
                os.Setenv(k, v)
            }
            defer func() {
                // Clean up environment
                for k := range tt.envVars {
                    os.Unsetenv(k)
                }
            }()
            
            _, err := LoadConfig()
            if (err != nil) != tt.wantErr {
                t.Errorf("LoadConfig() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

2. Mocking Configuration

Mock configuration for tests:

type MockConfigProvider struct {
    values map[string]string
}

func NewMockConfigProvider(values map[string]string) *MockConfigProvider {
    return &MockConfigProvider{values: values}
}

func (m *MockConfigProvider) Get(key string) (string, error) {
    if value, ok := m.values[key]; ok {
        return value, nil
    }
    return "", fmt.Errorf("key not found: %s", key)
}

func TestFeatureFlag(t *testing.T) {
    mock := NewMockConfigProvider(map[string]string{
        "enable_new_ui": "true",
        "enable_beta_api": "false",
    })
    
    features := NewFeatureManager(mock)
    
    assert.True(t, features.IsEnabled("new_ui"))
    assert.False(t, features.IsEnabled("beta_api"))
}

Next Steps