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
- Learn about Error Handling
- Explore Logging
- Study Security