Advanced Configuration Loading
Centralizes configuration resolution so deployments can override settings safely without code changes.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
package main
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// Config represents the application configuration
type Config struct {
Server struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
} `mapstructure:"server"`
Database struct {
DSN string `mapstructure:"dsn"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime_seconds"`
} `mapstructure:"database"`
Logging struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"` // "json" or "text"
Output string `mapstructure:"output"` // "stdout", "stderr", or file path
} `mapstructure:"logging"`
FeatureFlags struct {
EnableNewAPI bool `mapstructure:"enable_new_api"`
EnableMetrics bool `mapstructure:"enable_metrics"`
} `mapstructure:"feature_flags"`
}
// LoadConfig loads configuration from multiple sources with priority:
// 1. Command-line flags (highest)
// 2. Environment variables
// 3. Configuration file
// 4. Default values (lowest)
func LoadConfig(configPaths ...string) (*Config, error) {
// Initialize Viper
v := viper.New()
// 1. Set default values (lowest priority)
setDefaults(v)
// 2. Read configuration file (if provided)
if len(configPaths) > 0 {
for _, path := range configPaths {
// Check if file exists
if _, err := os.Stat(path); err == nil {
v.SetConfigFile(path)
break
}
}
} else {
// Set default config paths
v.SetConfigName("config") // name of config file (without extension)
v.SetConfigType("yaml") // config file type
v.AddConfigPath(".") // current directory
v.AddConfigPath("$HOME/.app/") // home directory
v.AddConfigPath("/etc/app/") // system config directory
}
// Read config file if available
if err := v.ReadInConfig(); err != nil {
var configFileNotFoundError viper.ConfigFileNotFoundError
if !errors.As(err, &configFileNotFoundError) {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
log.Println("No config file found, using defaults and environment variables")
} else {
log.Printf("Loaded config file: %s", v.ConfigFileUsed())
}
// 3. Bind environment variables (middle priority)
// Use APP_ prefix for all environment variables
v.SetEnvPrefix("APP")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // replace dots with underscores
v.AutomaticEnv() // read in environment variables that match
// Bind specific environment variables (optional, for clarity)
bindEnvVars(v)
// 4. Bind command-line flags (highest priority)
flags := pflag.NewFlagSet("app", pflag.ExitOnError)
bindFlags(v, flags)
// Parse command-line flags
if err := flags.Parse(os.Args[1:]); err != nil {
return nil, fmt.Errorf("failed to parse command-line flags: %w", err)
}
// Unmarshal config into struct
var config Config
if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Validate config
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return &config, nil
}
// setDefaults sets default configuration values
func setDefaults(v *viper.Viper) {
// Server defaults
v.SetDefault("server.host", "0.0.0.0")
v.SetDefault("server.port", 8080)
// Database defaults
v.SetDefault("database.max_open_conns", 10)
v.SetDefault("database.max_idle_conns", 5)
v.SetDefault("database.conn_max_lifetime_seconds", 300)
// Logging defaults
v.SetDefault("logging.level", "info")
v.SetDefault("logging.format", "text")
v.SetDefault("logging.output", "stdout")
// Feature flags defaults
v.SetDefault("feature_flags.enable_new_api", false)
v.SetDefault("feature_flags.enable_metrics", true)
}
// bindEnvVars binds environment variables to config keys
func bindEnvVars(v *viper.Viper) {
// Example explicit bindings (optional)
_ = v.BindEnv("server.port")
_ = v.BindEnv("database.dsn")
_ = v.BindEnv("logging.level")
_ = v.BindEnv("feature_flags.enable_new_api")
}
// bindFlags binds command-line flags to config keys
func bindFlags(v *viper.Viper, flags *pflag.FlagSet) {
// Server flags
flags.String("server.host", v.GetString("server.host"), "Server host address")
flags.Int("server.port", v.GetInt("server.port"), "Server port number")
// Database flags
flags.String("database.dsn", v.GetString("database.dsn"), "Database connection string")
flags.Int("database.max-open-conns", v.GetInt("database.max_open_conns"), "Maximum open database connections")
flags.Int("database.max-idle-conns", v.GetInt("database.max_idle_conns"), "Maximum idle database connections")
flags.Int("database.conn-max-lifetime-seconds", v.GetInt("database.conn_max_lifetime_seconds"), "Database connection max lifetime in seconds")
// Logging flags
flags.String("logging.level", v.GetString("logging.level"), "Logging level (debug, info, warn, error)")
flags.String("logging.format", v.GetString("logging.format"), "Logging format (text, json)")
flags.String("logging.output", v.GetString("logging.output"), "Logging output (stdout, stderr, file path)")
// Feature flags
flags.Bool("feature-flags.enable-new-api", v.GetBool("feature_flags.enable_new_api"), "Enable new API endpoint")
flags.Bool("feature-flags.enable-metrics", v.GetBool("feature_flags.enable_metrics"), "Enable metrics collection")
// Bind flags to Viper
_ = v.BindPFlag("server.host", flags.Lookup("server.host"))
_ = v.BindPFlag("server.port", flags.Lookup("server.port"))
_ = v.BindPFlag("database.dsn", flags.Lookup("database.dsn"))
_ = v.BindPFlag("database.max_open_conns", flags.Lookup("database.max-open-conns"))
_ = v.BindPFlag("database.max_idle_conns", flags.Lookup("database.max-idle-conns"))
_ = v.BindPFlag("database.conn_max_lifetime_seconds", flags.Lookup("database.conn-max-lifetime-seconds"))
_ = v.BindPFlag("logging.level", flags.Lookup("logging.level"))
_ = v.BindPFlag("logging.format", flags.Lookup("logging.format"))
_ = v.BindPFlag("logging.output", flags.Lookup("logging.output"))
_ = v.BindPFlag("feature_flags.enable_new_api", flags.Lookup("feature-flags.enable-new-api"))
_ = v.BindPFlag("feature_flags.enable_metrics", flags.Lookup("feature-flags.enable-metrics"))
}
// validateConfig validates the configuration
func validateConfig(config *Config) error {
// Validate server port
if config.Server.Port < 1 || config.Server.Port > 65535 {
return errors.New("server port must be between 1 and 65535")
}
// Validate logging level
validLogLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
}
if !validLogLevels[config.Logging.Level] {
return errors.New("logging level must be debug, info, warn, or error")
}
// Validate logging format
if config.Logging.Format != "text" && config.Logging.Format != "json" {
return errors.New("logging format must be text or json")
}
// Validate database connection pool settings
if config.Database.MaxOpenConns < 1 {
return errors.New("database max open conns must be at least 1")
}
if config.Database.MaxIdleConns < 0 {
return errors.New("database max idle conns cannot be negative")
}
if config.Database.MaxIdleConns > config.Database.MaxOpenConns {
return errors.New("database max idle conns cannot exceed max open conns")
}
// If DSN is provided, no additional validation (database driver will handle it)
return nil
}
// PrintConfig prints the configuration (hides sensitive data)
func PrintConfig(config *Config) {
// Create a copy to redact sensitive data
redactedConfig := *config
if redactedConfig.Database.DSN != "" {
redactedConfig.Database.DSN = "*** REDACTED ***"
}
log.Println("Application Configuration:")
fmt.Printf(" Server: %s:%d\n", redactedConfig.Server.Host, redactedConfig.Server.Port)
fmt.Printf(" Database:\n")
fmt.Printf(" Max Open Conns: %d\n", redactedConfig.Database.MaxOpenConns)
fmt.Printf(" Max Idle Conns: %d\n", redactedConfig.Database.MaxIdleConns)
fmt.Printf(" Conn Max Lifetime: %ds\n", redactedConfig.Database.ConnMaxLifetime)
fmt.Printf(" Logging:\n")
fmt.Printf(" Level: %s\n", redactedConfig.Logging.Level)
fmt.Printf(" Format: %s\n", redactedConfig.Logging.Format)
fmt.Printf(" Output: %s\n", redactedConfig.Logging.Output)
fmt.Printf(" Feature Flags:\n")
fmt.Printf(" Enable New API: %t\n", redactedConfig.FeatureFlags.EnableNewAPI)
fmt.Printf(" Enable Metrics: %t\n", redactedConfig.FeatureFlags.EnableMetrics)
}
func main() {
// Example usage:
// 1. Load config (supports config file, env vars, and flags)
// Example env vars: APP_SERVER_PORT=9090, APP_LOGGING_LEVEL=debug
// Example flags: --server.port=9090 --logging.level=debug
// Custom config file path (optional)
configFile := ""
if len(os.Args) > 1 && filepath.Ext(os.Args[1]) == ".yaml" {
configFile = os.Args[1]
os.Args = append(os.Args[:1], os.Args[2:]...) // Remove config file from args
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Print configuration (with sensitive data redacted)
PrintConfig(config)
// Use configuration in your application
log.Printf("Starting server on %s:%d", config.Server.Host, config.Server.Port)
// ... rest of your application ...
}How It Works
Centralizes configuration resolution so deployments can override settings safely without code changes.
Defines a Config struct with mapstructure tags, sets defaults, binds environment variables with prefixes, reads a config file if provided, parses command-line flags, unmarshals into the struct, validates required fields and acceptable ranges, and prints a summary or exits with wrapped errors.
Key Concepts
- 1Multiple sources merged with a clear priority order (flags > env > file > defaults).
- 2Mapstructure tags align nested fields with configuration keys.
- 3Validation enforces non-empty DSNs and sensible limits.
- 4Helpful error messages include which key failed or was missing.
When to Use This Pattern
- Bootstrapping microservices that run across development, staging, and production.
- CLI tools needing flexible overrides without recompiling.
- Twelve-factor apps that rely heavily on environment configuration.
- Testing different configurations locally via flags without touching files.
Best Practices
- Keep a single function responsible for loading and validating configuration.
- Document precedence so operators know how to override values.
- Fail fast when required secrets or ports are missing.
- Avoid global state; inject the configuration into the rest of the app.
Go Version1.18+
Difficultyadvanced
Production ReadyYes
Lines of Code263