Environment Variables

Master environment variable integration with hierarchical naming conventions

With a prefix such as MYAPP_, the config package maps environment variables to nested keys (for example MYAPP_SERVER_PORTserver.port). This matches the Twelve-Factor App idea of storing config in the environment.

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.