Environment Variables

Master environment variable integration with hierarchical naming conventions

The config package provides powerful environment variable support. It automatically maps environment variables to nested configuration structures. This follows the Twelve-Factor App methodology for configuration management.

Basic Usage

Enable environment variable support with a custom prefix:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("MYAPP_"),  // Only env vars with MYAPP_ prefix
)

The prefix helps avoid conflicts with system or other application variables.

Naming Convention

The config package uses a hierarchical naming convention where underscores (_) in environment variable names create nested configuration structures.

Transformation Rules

  1. Strip prefix: Remove the configured prefix like MYAPP_.
  2. Convert to lowercase: DATABASE_HOSTdatabase_host.
  3. Split by underscores: database_host["database", "host"].
  4. Filter empty parts: Consecutive underscores create no extra levels.
  5. Create dot notation: ["database", "host"]database.host.

Visualization

graph TB
    EnvVar[MYAPP_DATABASE_HOST=localhost] --> StripPrefix[Strip Prefix MYAPP_]
    StripPrefix --> Lowercase[database_host]
    Lowercase --> SplitUnderscore[Split by underscore]
    SplitUnderscore --> FilterEmpty[Filter empty parts]
    FilterEmpty --> DotNotation[database.host = localhost]

Examples

Environment VariableConfiguration PathValue
MYAPP_SERVER_PORTserver.port8080
MYAPP_DATABASE_HOSTdatabase.hostlocalhost
MYAPP_DATABASE_USER_NAMEdatabase.user.nameadmin
MYAPP_FOO__BARfoo.barvalue
MYAPP_A_B_C_Da.b.c.dnested

Basic Example

export MYAPP_SERVER_HOST=localhost
export MYAPP_SERVER_PORT=8080
export MYAPP_DEBUG=true
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
)
cfg.Load(context.Background())

host := cfg.String("server.host")   // "localhost"
port := cfg.Int("server.port")      // 8080
debug := cfg.Bool("debug")          // true

Nested Configuration

Environment variables naturally create nested structures:

export MYAPP_DATABASE_PRIMARY_HOST=db1.example.com
export MYAPP_DATABASE_PRIMARY_PORT=5432
export MYAPP_DATABASE_REPLICA_HOST=db2.example.com
export MYAPP_DATABASE_REPLICA_PORT=5432

Access nested values:

primaryHost := cfg.String("database.primary.host")  // "db1.example.com"
replicaHost := cfg.String("database.replica.host")  // "db2.example.com"

Struct Field Mapping

When using struct binding, environment variables map directly to struct fields using the config tag:

type Config struct {
    Port     int    `config:"port"`
    Host     string `config:"host"`
    Database struct {
        Host     string `config:"host"`
        Port     int    `config:"port"`
        Username string `config:"username"`
        Password string `config:"password"`
    } `config:"database"`
}

Required environment variables:

export MYAPP_PORT=8080
export MYAPP_HOST=localhost
export MYAPP_DATABASE_HOST=db.example.com
export MYAPP_DATABASE_PORT=5432
export MYAPP_DATABASE_USERNAME=admin
export MYAPP_DATABASE_PASSWORD=secret123

Usage:

var appConfig Config
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
    config.WithBinding(&appConfig),
)

if err := cfg.Load(context.Background()); err != nil {
    log.Fatalf("config error: %v", err)
}

// appConfig is now populated from environment variables

Advanced Nested Structures

For complex applications with deeply nested configuration:

type AppConfig struct {
    Server struct {
        Host string `config:"host"`
        Port int    `config:"port"`
        TLS  struct {
            Enabled  bool   `config:"enabled"`
            CertFile string `config:"cert_file"`
            KeyFile  string `config:"key_file"`
        } `config:"tls"`
    } `config:"server"`
    Database struct {
        Primary struct {
            Host     string `config:"host"`
            Port     int    `config:"port"`
            Database string `config:"database"`
        } `config:"primary"`
        Replica struct {
            Host     string `config:"host"`
            Port     int    `config:"port"`
            Database string `config:"database"`
        } `config:"replica"`
    } `config:"database"`
}

Environment variables:

export MYAPP_SERVER_HOST=0.0.0.0
export MYAPP_SERVER_PORT=8080
export MYAPP_SERVER_TLS_ENABLED=true
export MYAPP_SERVER_TLS_CERT_FILE=/etc/ssl/certs/server.crt
export MYAPP_SERVER_TLS_KEY_FILE=/etc/ssl/private/server.key
export MYAPP_DATABASE_PRIMARY_HOST=primary.db.example.com
export MYAPP_DATABASE_PRIMARY_PORT=5432
export MYAPP_DATABASE_PRIMARY_DATABASE=myapp
export MYAPP_DATABASE_REPLICA_HOST=replica.db.example.com
export MYAPP_DATABASE_REPLICA_PORT=5432
export MYAPP_DATABASE_REPLICA_DATABASE=myapp

Edge Cases

Consecutive Underscores

Multiple consecutive underscores are filtered to prevent empty parts:

export MYAPP_FOO__BAR=value    # Becomes: foo.bar = "value"
export MYAPP_A___B=value       # Becomes: a.b = "value"

Type Conflicts

If environment variables create conflicts between scalar and nested values, the nested structure takes precedence:

export MYAPP_FOO=scalar_value
export MYAPP_FOO_BAR=nested_value
# Result: foo.bar = "nested_value" (scalar "foo" is overwritten)

Whitespace Handling

Keys and values are automatically trimmed:

export MYAPP_KEY="  value  "   # Becomes: key = "value"

Best Practices

1. Use Descriptive Prefixes

Always use application-specific prefixes to avoid conflicts:

# Good - Application-specific
export MYAPP_DATABASE_HOST=localhost
export WEBAPP_DATABASE_HOST=localhost

# Avoid - Too generic
export DATABASE_HOST=localhost

2. Consistent Naming

Use consistent patterns across your application:

# Consistent pattern
export MYAPP_SERVER_HOST=localhost
export MYAPP_SERVER_PORT=8080
export MYAPP_SERVER_TIMEOUT=30s

3. Document Your Variables

Document required and optional environment variables:

# Required environment variables:
# MYAPP_SERVER_HOST - Server hostname (default: localhost)
# MYAPP_SERVER_PORT - Server port (default: 8080)
# MYAPP_DATABASE_HOST - Database hostname (required)
# MYAPP_DATABASE_PORT - Database port (default: 5432)

4. Validate Configuration

Use struct validation to ensure required variables are set:

func (c *Config) Validate() error {
    if c.Server.Host == "" {
        return errors.New("MYAPP_SERVER_HOST is required")
    }
    if c.Server.Port <= 0 {
        return errors.New("MYAPP_SERVER_PORT must be positive")
    }
    return nil
}

Merging with Other Sources

Environment variables can override file-based configuration:

cfg := config.MustNew(
    config.WithFile("config.yaml"),      // Base configuration
    config.WithFile("config.prod.yaml"), // Production overrides
    config.WithEnv("MYAPP_"),            // Environment overrides all files
)

Source precedence: Later sources override earlier ones. Environment variables, being last, have the highest priority.

Complete Example

package main

import (
    "context"
    "log"
    "rivaas.dev/config"
)

type AppConfig struct {
    Server struct {
        Host string `config:"host"`
        Port int    `config:"port"`
    } `config:"server"`
    Database struct {
        Host     string `config:"host"`
        Port     int    `config:"port"`
        Username string `config:"username"`
        Password string `config:"password"`
    } `config:"database"`
}

func (c *AppConfig) Validate() error {
    if c.Database.Host == "" {
        return errors.New("database host is required")
    }
    if c.Database.Username == "" {
        return errors.New("database username is required")
    }
    return nil
}

func main() {
    var appConfig AppConfig
    
    cfg := config.MustNew(
        config.WithFile("config.yaml"),  // Base config
        config.WithEnv("MYAPP_"),        // Override with env vars
        config.WithBinding(&appConfig),
    )

    if err := cfg.Load(context.Background()); err != nil {
        log.Fatalf("failed to load config: %v", err)
    }

    log.Printf("Server: %s:%d", appConfig.Server.Host, appConfig.Server.Port)
    log.Printf("Database: %s:%d", appConfig.Database.Host, appConfig.Database.Port)
}

Next Steps

For technical details on the environment variable codec, see Codecs Reference.