Enhance Configuration System with Environment Variable Priority Handling

- Updated the configuration loading mechanism to prioritize OS environment variables over .env file values, improving flexibility and security.
- Added a new function, GetEnvWithPriority, to explicitly check the source of configuration values.
- Revised documentation to reflect the new priority system, including detailed examples and usage instructions for mixed configuration approaches.
- Enhanced error handling for .env file loading, ensuring graceful handling of missing files without disrupting the application flow.
This commit is contained in:
n.nakhostin
2025-09-15 14:52:12 +03:30
parent c701053609
commit 05a307e345
2 changed files with 175 additions and 16 deletions
+34 -5
View File
@@ -65,15 +65,24 @@ type RateLimitConfig struct {
Burst int `env:"RATE_LIMIT_BURST"`
}
// LoadConfig loads configuration from .env file
// LoadConfig loads configuration with priority: OS environment variables > .env file
// OS environment variables take precedence over .env file values
func LoadConfig[T any](path string, config T) (T, error) {
// Load .env file
// First, try to load .env file (this will not override existing OS env vars)
envPath := fmt.Sprintf("%s/.env", path)
if err := gotenv.Load(envPath); err != nil {
return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err)
// Check if .env file exists before trying to load it
if _, err := os.Stat(envPath); err == nil {
// Load .env file - gotenv.Load respects existing environment variables
// and will not override them with values from the .env file
if err := gotenv.Load(envPath); err != nil {
return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err)
}
}
// If .env file doesn't exist, we continue with OS environment variables only
// Parse configuration using reflection
// This will use OS env vars first, then fall back to values loaded from .env
if err := parseConfig(config); err != nil {
return config, fmt.Errorf("failed to parse configuration: %w", err)
}
@@ -82,6 +91,9 @@ func LoadConfig[T any](path string, config T) (T, error) {
}
// parseConfig uses reflection to parse environment variables into the config struct
// Environment variables are read with the following priority:
// 1. OS environment variables (highest priority)
// 2. Values from .env file (loaded by gotenv.Load, lower priority)
func parseConfig(config interface{}) error {
v := reflect.ValueOf(config)
if v.Kind() != reflect.Ptr {
@@ -122,6 +134,7 @@ func parseStruct(v reflect.Value) error {
}
// Get environment variable value
// os.Getenv will return OS env vars first, then values loaded from .env
envValue := os.Getenv(envTag)
if envValue == "" {
continue
@@ -129,7 +142,7 @@ func parseStruct(v reflect.Value) error {
// Parse and set the field value
if err := setFieldValue(field, envValue); err != nil {
return fmt.Errorf("failed to set field %s: %w", fieldType.Name, err)
return fmt.Errorf("failed to set field %s (env: %s): %w", fieldType.Name, envTag, err)
}
}
@@ -176,6 +189,22 @@ func setFieldValue(field reflect.Value, value string) error {
return nil
}
// GetEnvWithPriority gets environment variable with explicit priority handling
// Returns the value and a boolean indicating the source (true = OS env, false = .env file)
func GetEnvWithPriority(key string) (string, bool) {
// Check if the value exists in OS environment
if value, exists := os.LookupEnv(key); exists {
return value, true // From OS environment
}
// If not in OS environment, it might be from .env file
if value := os.Getenv(key); value != "" {
return value, false // From .env file
}
return "", false
}
// Helper function to get environment variable with default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {