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