Skip to content

FAQ

Basic Usage

Should I choose Load() or New()?

env.Load() (global mode) is suitable for simple applications: load once, use package-level functions globally. It automatically applies variables to os.Environ.

env.New() (instance mode) is suitable for testing and multi-configuration scenarios: creates isolated instances, does not auto-apply, requires explicit Close().

go
// Simple application → global mode
env.Load(".env")
port := env.GetInt("PORT", 8080)

// Testing / multi-configuration → instance mode
loader, _ := env.New(env.TestingConfig())
defer loader.Close()
port := loader.GetInt("PORT", 8080)

TIP

If unsure, start with env.Load(). Switch to env.New() when you encounter test isolation or multi-configuration needs.

Why can Load() only be called once?

Load() sets the global default loader (singleton pattern); repeated calls return ErrAlreadyInitialized. This is a design decision: avoiding accidentally overwriting already-loaded configuration at runtime.

go
// First call — succeeds
env.Load(".env")

// Second call — returns error
err := env.Load(".env.production")
// err == env.ErrAlreadyInitialized

Solutions:

go
// Solution 1: Load multiple files at once (recommended)
env.Load(".env", ".env.production")

// Solution 2: Reset first when re-initialization is needed
env.ResetDefaultLoader()  // Primarily for testing
env.Load(".env.production")

What happens when the .env file doesn't exist?

Default behavior: silently skips, no error. This supports the flexible "load if present, ignore if absent" deployment pattern.

go
// DefaultConfig — silently skips when file doesn't exist
env.Load(".env", ".env.local")
// Even if neither file exists, no error is returned

If you want an error when the file doesn't exist (recommended for production):

go
cfg := env.ProductionConfig()
// FailOnMissingFile defaults to true (only for ProductionConfig)
loader, _ := env.New(cfg)

How to access nested JSON/YAML values?

Nested JSON/YAML structures are automatically flattened to underscore-separated key names:

json
{
  "database": {
    "host": "localhost",
    "port": 5432
  }
}
Stored as: DATABASE_HOST=localhost, DATABASE_PORT=5432

The three access methods are equivalent:

go
host := env.GetString("DATABASE_HOST")  // Flat key (recommended)
host := env.GetString("database.host")  // Dot path
host := env.GetString("DATABASE.HOST")  // Uppercase dot path

Types and Generics

Why is GetSlice a generic function instead of a method?

Go does not support type parameters on methods. GetSlice[T] must be a function, not a method:

go
// ❌ Method approach — compile error (Go doesn't support this)
// loader.GetSlice[int]("PORTS")

// ✅ Function approach — works
env.GetSliceFrom[int](loader, "PORTS")

// ✅ Package-level function
env.GetSlice[int]("PORTS")

How does GetSlice parse slice values?

Searches by priority:

  1. Indexed keys (recommended): KEY_0, KEY_1, KEY_2...
  2. Comma-separated: KEY=val1,val2,val3
bash
# Method 1: Indexed keys
HOSTS_0=localhost
HOSTS_1=example.com

# Method 2: Comma-separated
HOSTS=localhost,example.com
go
hosts := env.GetSlice[string]("HOSTS") // ["localhost", "example.com"]

What boolean value formats are supported?

GetBool is case-insensitive and supports the following values:

TruthyFalsy
true, 1, yes, on, enabledfalse, 0, no, off, disabled

Concurrency and Thread Safety

Can I call Get from multiple goroutines simultaneously?

Yes. All Loader methods are thread-safe. The library uses sharded locks to optimize read/write performance in high-concurrency scenarios.

go
// Safe concurrent access
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        _ = env.GetString("KEY") // Thread-safe
    }()
}
wg.Wait()

What happens when calling Get after Loader.Close()?

Returns zero values, doesn't panic. The Loader enters a read-only degraded mode after closing:

go
loader, _ := env.New()
defer loader.Close()

val := loader.GetString("KEY") // Returns normally

// After Close()
val = loader.GetString("KEY")  // Returns "" (zero value)
err := loader.Set("KEY", "v")  // Returns ErrClosed

SecureValue

What's the difference between Release and Close?

MethodZeros MemoryUnlocks MemoryReturns to Pool
Release()
Close()

Using Release() is recommended — it returns the object to the pool, reducing GC pressure. Close() is for scenarios where pooling is not needed.

Does SecureValue auto-zero when garbage collected?

Yes. SecureValue sets a finalizer that automatically zeros memory during garbage collection. However, it's recommended to explicitly call Release() or Close() to ensure timely cleanup, rather than relying on GC's non-deterministic timing.

go
// ✅ Recommended: explicit release
sv := env.GetSecure("API_KEY")
defer sv.Release()

// ⚠️ Relying on GC — not recommended but safe
sv := env.GetSecure("API_KEY")
// Eventually zeroed by GC, but timing is uncertain

How to safely log?

Use Masked() or masking utility functions; never directly output Reveal() values:

go
sv := env.GetSecure("API_KEY")
defer sv.Release()

// ✅ Safe — masked output
log.Printf("API Key: %s", sv.Masked())    // [SECURE:32 bytes locked]
log.Printf("API Key: %s", sv)              // Same (String() returns Masked())

// ✅ Safe — masking tools
masked := env.MaskValue("API_KEY", "sk-xxx") // sk-******************************
clean := env.SanitizeForLog(logMessage)       // Auto-detect and mask

// ❌ Dangerous — plaintext leak
plaintext := sv.Reveal()
log.Printf("API Key: %s", plaintext) // Don't do this

Configuration and Validation

How to load only variables with a specific prefix?

Use the Prefix field to filter:

go
cfg := env.DefaultConfig()
cfg.Prefix = "MYAPP_"  // Only load variables starting with MYAPP_
loader, _ := env.New(cfg)
loader.LoadFiles(".env")
bash
# .env file content
MYAPP_HOST=localhost    # ✅ Loaded
MYAPP_PORT=8080         # ✅ Loaded
OTHER_KEY=value         # ❌ Ignored (no MYAPP_ prefix)

How to prevent configuration overrides?

OverwriteExisting controls whether to overwrite existing variables:

go
// Default: no overwriting (safe)
cfg := env.DefaultConfig()
cfg.OverwriteExisting = false

// Development: allow overwriting
cfg := env.DevelopmentConfig()
// OverwriteExisting = true

When does RequiredKeys validation execute?

Only checked when Validate() is explicitly called, not automatically triggered during loading:

go
cfg := env.ProductionConfig()
cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}
loader, _ := env.New(cfg)
loader.LoadFiles(".env")

// Explicit validation
if err := loader.Validate(); err != nil {
    if errors.Is(err, env.ErrMissingRequired) {
        log.Fatal("Missing required environment variables")
    }
}

Testing

How to isolate the environment in tests?

Use TestingConfig() + independent Loader instances:

go
func TestConfig(t *testing.T) {
    cfg := env.TestingConfig()
    cfg.OverwriteExisting = true

    loader, err := env.New(cfg)
    if err != nil {
        t.Fatal(err)
    }
    defer loader.Close()

    // Each test is independent, no interference
    loader.Set("KEY", "test-value")
    val := loader.GetString("KEY")
    // Test...
}

How to reset global mode tests?

Use ResetDefaultLoader():

go
func TestGlobalMode(t *testing.T) {
    // Clean up previous test state
    env.ResetDefaultLoader()
    defer env.ResetDefaultLoader()

    env.Load(".env.test")
    // Test...
}

TIP

See the Testing guide for complete testing documentation.