main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
package main

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

// EnvVar represents an environment variable configuration
type EnvVar struct {
	Name         string        // Name of the environment variable
	Description  string        // Human-readable description
	Type         string        // Type: string, int, bool, float, duration
	Default      interface{}   // Default value (nil for required)
	Required     bool          // Whether the variable is required
	Validation   func(interface{}) error // Custom validation function
}

// EnvManager manages environment variable loading and validation
type EnvManager struct {
	variables map[string]*EnvVar // Registered environment variables
	values    map[string]interface{} // Loaded values
	errors    []error           // Collection of errors during loading
}

// NewEnvManager creates a new environment variable manager
func NewEnvManager() *EnvManager {
	return &EnvManager{
		variables: make(map[string]*EnvVar),
		values:    make(map[string]interface{}),
	}
}

// Register adds an environment variable to the manager
func (e *EnvManager) Register(envVar EnvVar) {
	// Validate basic configuration
	if envVar.Name == "" {
		e.errors = append(e.errors, errors.New("environment variable name cannot be empty"))
		return
	}

	if envVar.Type == "" {
		envVar.Type = "string" // Default to string type
	}

	// Check for duplicate registration
	if _, exists := e.variables[envVar.Name]; exists {
		e.errors = append(e.errors, fmt.Errorf("environment variable '%s' already registered", envVar.Name))
		return
	}

	e.variables[envVar.Name] = &envVar
}

// Load reads and validates all registered environment variables
func (e *EnvManager) Load() error {
	// Reset values and errors
	e.values = make(map[string]interface{})
	e.errors = nil

	// Process each registered variable
	for name, envVar := range e.variables {
		e.processVariable(name, envVar)
	}

	// Return aggregated errors if any
	if len(e.errors) > 0 {
		errMsg := "failed to load environment variables:\n"
		for _, err := range e.errors {
			errMsg += fmt.Sprintf("  - %v\n", err)
		}
		return errors.New(errMsg)
	}

	return nil
}

// processVariable loads and validates a single environment variable
func (e *EnvManager) processVariable(name string, envVar *EnvVar) {
	// Get value from environment
	strValue := os.Getenv(name)

	// Check if required
	if strValue == "" {
		if envVar.Required {
			e.errors = append(e.errors, fmt.Errorf("required environment variable '%s' is not set (%s)", name, envVar.Description))
			return
		}

		// Use default value if available
		if envVar.Default != nil {
			e.values[name] = envVar.Default
			return
		}

		// Use zero value for optional variables with no default
		e.values[name] = getZeroValue(envVar.Type)
		return
	}

	// Convert to specified type
	convertedValue, err := convertValue(strValue, envVar.Type)
	if err != nil {
		e.errors = append(e.errors, fmt.Errorf("failed to convert '%s' (value: '%s') to %s: %v", name, strValue, envVar.Type, err))
		return
	}

	// Run custom validation if provided
	if envVar.Validation != nil {
		if err := envVar.Validation(convertedValue); err != nil {
			e.errors = append(e.errors, fmt.Errorf("validation failed for '%s': %v", name, err))
			return
		}
	}

	// Store the converted value
	e.values[name] = convertedValue
}

// convertValue converts a string to the specified type
func convertValue(value string, typ string) (interface{}, error) {
	switch strings.ToLower(typ) {
	case "string":
		return value, nil
	case "int":
		return strconv.Atoi(value)
	case "bool":
		return strconv.ParseBool(value)
	case "float":
		return strconv.ParseFloat(value, 64)
	case "duration":
		return time.ParseDuration(value)
	default:
		return nil, fmt.Errorf("unsupported type '%s'", typ)
	}
}

// getZeroValue returns the zero value for the specified type
func getZeroValue(typ string) interface{} {
	switch strings.ToLower(typ) {
	case "string":
		return ""
	case "int":
		return 0
	case "bool":
		return false
	case "float":
		return 0.0
	case "duration":
		return 0 * time.Second
	default:
		return nil
	}
}

// GetString retrieves a string value from the loaded environment variables
func (e *EnvManager) GetString(name string) (string, error) {
	value, exists := e.values[name]
	if !exists {
		return "", fmt.Errorf("environment variable '%s' not found", name)
	}

	strVal, ok := value.(string)
	if !ok {
		return "", fmt.Errorf("environment variable '%s' is not a string", name)
	}

	return strVal, nil
}

// GetInt retrieves an int value from the loaded environment variables
func (e *EnvManager) GetInt(name string) (int, error) {
	value, exists := e.values[name]
	if !exists {
		return 0, fmt.Errorf("environment variable '%s' not found", name)
	}

	intVal, ok := value.(int)
	if !ok {
		return 0, fmt.Errorf("environment variable '%s' is not an int", name)
	}

	return intVal, nil
}

// GetBool retrieves a bool value from the loaded environment variables
func (e *EnvManager) GetBool(name string) (bool, error) {
	value, exists := e.values[name]
	if !exists {
		return false, fmt.Errorf("environment variable '%s' not found", name)
	}

	boolVal, ok := value.(bool)
	if !ok {
		return false, fmt.Errorf("environment variable '%s' is not a bool", name)
	}

	return boolVal, nil
}

// GetFloat retrieves a float64 value from the loaded environment variables
func (e *EnvManager) GetFloat(name string) (float64, error) {
	value, exists := e.values[name]
	if !exists {
		return 0.0, fmt.Errorf("environment variable '%s' not found", name)
	}

	floatVal, ok := value.(float64)
	if !ok {
		return 0.0, fmt.Errorf("environment variable '%s' is not a float", name)
	}

	return floatVal, nil
}

// GetDuration retrieves a time.Duration value from the loaded environment variables
func (e *EnvManager) GetDuration(name string) (time.Duration, error) {
	value, exists := e.values[name]
	if !exists {
		return 0, fmt.Errorf("environment variable '%s' not found", name)
	}

	durVal, ok := value.(time.Duration)
	if !ok {
		return 0, fmt.Errorf("environment variable '%s' is not a duration", name)
	}

	return durVal, nil
}

func main() {
	// Initialize manager
	manager := NewEnvManager()

	// Register environment variables
	manager.Register(EnvVar{
		Name:        "PORT",
		Description: "HTTP server port",
		Type:        "int",
		Default:     8080,
		Required:    false,
		Validation: func(val interface{}) error {
			port := val.(int)
			if port < 1 || port > 65535 {
				return fmt.Errorf("port must be between 1 and 65535")
			}
			return nil
		},
	})

	manager.Register(EnvVar{
		Name:        "DEBUG",
		Description: "Enable debug mode",
		Type:        "bool",
		Default:     false,
		Required:    false,
	})

	manager.Register(EnvVar{
		Name:        "DB_URL",
		Description: "Database connection URL",
		Type:        "string",
		Required:    true,
	})

	// Load variables
	if err := manager.Load(); err != nil {
		fmt.Printf("Error loading env vars: %v\n", err)
		return
	}

	// Retrieve values
	port, _ := manager.GetInt("PORT")
	debug, _ := manager.GetBool("DEBUG")
	dbURL, _ := manager.GetString("DB_URL")

	fmt.Printf("Loaded config: PORT=%d, DEBUG=%t, DB_URL=%s\n", port, debug, dbURL)
}

How It Works

Typed environment loader that reads environment variables with defaults, required checks, and per-type parsers for integers, booleans, floats, and durations.

Helper functions fetch environment values, convert to desired types with strconv and time.ParseDuration, apply defaults, and return descriptive errors when validation fails; sample usage shows multiple variable types.

Key Concepts

  • 1Per-type parsers keep conversions centralized and testable.
  • 2Required and optional helpers reduce boilerplate checks.
  • 3Error messages name the environment variable and the failed value.

When to Use This Pattern

  • Configuring services via environment in containers.
  • CLI tools needing typed flags from environment overrides.
  • Bootstrap scripts that must fail fast on misconfiguration.

Best Practices

  • Validate ranges such as ports and timeouts after parsing.
  • Avoid silent fallbacks when a required variable is missing.
  • Document all environment keys and defaults for operators.
Go Version1.18
Difficultyintermediate
Production ReadyYes
Lines of Code281