Loader API
Complete method reference for the Loader type. Loader is the core type of the env library, providing environment variable loading, storage, and access functionality.
Thread safety
All methods of Loader are thread-safe and can be called concurrently from multiple goroutines.
Type Definition
type Loader struct {
// Contains private fields
}
// Compile-time interface implementation checks
var _ EnvLoader = (*Loader)(nil)
var _ io.Closer = (*Loader)(nil)Creation
New
func New(cfg ...Config) (*Loader, error)Creates a new loader instance.
Parameters:
cfg- optional configuration options. When not provided or a zero-value Config is passed,DefaultConfig()is automatically used.
Returns:
*Loader- loader instanceerror- configuration validation error
Behavior:
- Validates configuration
- Creates internal components (validator, auditor, expander)
- Automatically loads files if
cfg.Filenamesis non-empty - Automatically applies to system environment if
cfg.AutoApplyis true
// Use default configuration
loader, err := env.New()
// Use custom configuration
cfg := env.DefaultConfig()
cfg.Filenames = []string{".env"}
cfg.AutoApply = true
loader, err := env.New(cfg)
if err != nil {
panic(err)
}
defer loader.Close()File Loading
LoadFiles
func (l *Loader) LoadFiles(filenames ...string) errorLoads one or more configuration files.
Parameters:
filenames- list of file paths; defaults to loading.envwhen empty
Returns:
error- load error
Behavior:
- Loads files in order; by default (
OverwriteExisting=false) the first-loaded file wins — same-name keys in later files are skipped; withOverwriteExisting=true, later files overwrite earlier ones (built into the Dev/Testing presets) - Auto-detects file format (.env, JSON, YAML)
- Determines behavior for missing files based on
FailOnMissingFileconfig - If
AutoApplyis true, automatically applies after loading
// Load default .env file
err := loader.LoadFiles()
// Load specified files
err := loader.LoadFiles(".env", ".env.local")
// Mixed formats
err := loader.LoadFiles("config.env", "settings.json", "secrets.yaml")Error types:
ErrFileNotFound- file not found (whenFailOnMissingFile=true)ErrFileTooLarge- file exceeds size limitErrClosed- loader has been closed*ParseError- parse error*JSONError- JSON parse error*YAMLError- YAML parse error*SecurityError- file path security check failed (e.g., path traversal attack)
Format detection rules:
| Extension | Format |
|---|---|
.env | FormatEnv |
.json | FormatJSON |
.yaml, .yml | FormatYAML |
| Other | FormatAuto (uses .env parser) |
Getting Values
Key Resolution
All getter methods support smart key resolution:
| Input Key | Resolved Result |
|---|---|
"DATABASE_HOST" | "DATABASE_HOST" (exact match) |
"database.host" | "DATABASE_HOST" (dots to underscores) |
"app.name" | "APP_NAME" (uppercase + underscores) |
"servers.0.host" | "SERVERS_0_HOST" (array index) |
Resolution order:
- Exact match - directly look up the key name
- Uppercase conversion - simple keys try uppercase version
- Path resolution - dot paths are converted to underscore format
- Index fallback - index access falls back to comma-separated values
GetString
func (l *Loader) GetString(key string, defaultValue ...string) stringGets a string value. Supports dot-path resolution.
Parameters:
key- key name (supports exact match, uppercase conversion, dot path)defaultValue- optional default value
Returns:
string- value or default value (returns empty string when not found and no default)
// Basic usage
host := loader.GetString("HOST", "localhost")
// Dot-path access (JSON/YAML nested structures)
dbHost := loader.GetString("database.host", "localhost")
appName := loader.GetString("app.name")
// Returns empty string when no default
value := loader.GetString("NON_EXISTENT") // ""GetInt
func (l *Loader) GetInt(key string, defaultValue ...int64) int64Gets an integer value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)defaultValue- optional default value, typeint64
Returns:
int64- value or default value (returns 0 when not found and no default)
port := loader.GetInt("PORT", 8080)
maxConn := loader.GetInt("database.max_connections", 10)
// Returns 0 when no default
value := loader.GetInt("NON_EXISTENT") // 0GetBool
func (l *Loader) GetBool(key string, defaultValue ...bool) boolGets a boolean value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)defaultValue- optional default value
Returns:
bool- value or default value (returns false when not found and no default)
Supported values:
- Truthy:
true,1,yes,on,enabled - Falsy:
false,0,no,off,disabled
debug := loader.GetBool("DEBUG", false)
cacheEnabled := loader.GetBool("cache.enabled", true)
// Returns false when no default
value := loader.GetBool("NON_EXISTENT") // falseGetUint64
func (l *Loader) GetUint64(key string, defaultValue ...uint64) uint64Gets an unsigned integer value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)defaultValue- optional default value, typeuint64
Returns:
uint64- value or default value (returns 0 when not found and no default)
port := loader.GetUint64("PORT", 8080)
maxSize := loader.GetUint64("MAX_SIZE", 1024)
// Returns 0 when no default
value := loader.GetUint64("NON_EXISTENT") // 0GetFloat64
func (l *Loader) GetFloat64(key string, defaultValue ...float64) float64Gets a floating-point value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)defaultValue- optional default value, typefloat64
Returns:
float64- value or default value (returns 0 when not found and no default)
rate := loader.GetFloat64("RATE", 0.5)
threshold := loader.GetFloat64("THRESHOLD")
// Returns 0 when no default
value := loader.GetFloat64("NON_EXISTENT") // 0GetDuration
func (l *Loader) GetDuration(key string, defaultValue ...time.Duration) time.DurationGets a duration value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)defaultValue- optional default value
Returns:
time.Duration- value or default value (returns 0 when not found and no default)
Supported formats: ns, us, ms, s, m, h (e.g., 30s, 5m, 1h30m)
timeout := loader.GetDuration("TIMEOUT", 30*time.Second)
ttl := loader.GetDuration("cache.ttl", 5*time.Minute)
// Returns 0 when no default
value := loader.GetDuration("NON_EXISTENT") // 0GetSecure
func (l *Loader) GetSecure(key string) *SecureValueGets a secure value (sensitive data protection).
Parameters:
key- key name
Returns:
*SecureValue- defensive copy of the secure value; caller is responsible for releasing; returns nil if the key doesn't exist or loader is closed
secret := loader.GetSecure("API_SECRET")
if secret != nil {
defer secret.Release()
value := secret.Reveal() // plaintext value
masked := secret.Masked() // [SECURE:32 bytes]
}WARNING
You must call Release() or Close() after use to release resources.
Defensive copies
GetSecure returns a copy of the original value, independent from the parent Loader. The caller is responsible for calling Release() or Close().
TIP
See SecureValue API for complete documentation.
Getting Slice Values
Loader does not provide a slice getter method (Go does not support generic methods). Use the standalone generic function GetSliceFrom[T] to get slices from a Loader instance:
// Use standalone generic function
hosts := env.GetSliceFrom[string](loader, "HOSTS")
ports := env.GetSliceFrom[int64](loader, "PORTS", []int64{80})
portsInt := env.GetSliceFrom[int](loader, "PORTS") // also supports intSupported types: string, int, int64, uint, uint64, bool, float64, time.Duration
TIP
See Package Functions - GetSliceFrom for complete documentation.
Lookup
func (l *Loader) Lookup(key string) (string, bool)Checks whether a key exists and gets its value. Supports dot-path resolution.
Parameters:
key- key name (supports dot path)
Returns:
string- value (returned as stored, no trimming:.envvalues are trimmed at parse time, JSON/YAML values may keep surrounding whitespace)bool- whether it exists
value, exists := loader.Lookup("API_KEY")
if !exists {
// key does not exist
}
// Dot path
if value, exists := loader.Lookup("database.host"); exists {
fmt.Println(value)
}
// Index access (falls back to comma-separated value)
// HOSTS=localhost,example.com
if value, exists := loader.Lookup("hosts.0"); exists {
fmt.Println(value) // "localhost"
}Set and Delete
Set
func (l *Loader) Set(key, value string) errorSets an environment variable.
Parameters:
key- key namevalue- value
Returns:
error- set error
Behavior:
- Validates key name validity
- If
ValidateValuesis true, validates value safety - If
OverwriteExistingis false and key already exists, skips (returns nil) - If
AutoApplyis true, also sets in the system environment
err := loader.Set("CUSTOM_KEY", "value")
if err != nil {
// Handle error
}Error types:
*ValidationError- invalid key name format (Field="key")*SecurityError- key is forbidden (matchable witherrors.Is(err, env.ErrSecurityViolation))ErrInvalidValue- invalid value (whenValidateValuesis true, value contains null bytes, control characters, or other unsafe content)ErrClosed- loader has been closed
Delete
func (l *Loader) Delete(key string) errorDeletes an environment variable.
Parameters:
key- key name
Returns:
error- delete error
Behavior:
- If the variable has been applied to the system environment, also removes it from the system environment
err := loader.Delete("TEMP_KEY")
if err != nil {
panic(err)
}Collection Operations
Keys
func (l *Loader) Keys() []stringGets all key names.
Returns:
[]string- key name list; returns nil if loader is closed
keys := loader.Keys()
for _, key := range keys {
fmt.Println(key)
}All
func (l *Loader) All() map[string]stringGets all key-value pairs.
Returns:
map[string]string- key-value mapping; returns nil if loader is closed
all := loader.All()
for key, value := range all {
fmt.Printf("%s=%s\n", key, value)
}Len
func (l *Loader) Len() intGets the variable count.
Returns:
int- variable count; returns 0 if loader is closed
count := loader.Len()
fmt.Printf("Loaded %d variables\n", count)Apply to System
Apply
func (l *Loader) Apply() errorApplies variables to the system environment (os.Environ).
Returns:
error- apply error
Behavior:
- Iterates over all loaded variables
- Determines whether to overwrite existing system environment variables based on
OverwriteExistingconfig - After applying, accessible via
os.Getenv()
Error types:
ErrClosed- loader has been closed- Wrapped
oserror - failed to set environment variable (key name masked, sensitive key names not exposed in error message)
err := loader.Apply()
if err != nil {
panic(err)
}
// After that, os.Getenv() can access it too
host := os.Getenv("HOST")IsApplied
func (l *Loader) IsApplied() boolChecks whether variables have been applied to the system environment.
Returns:
bool- whether applied
if loader.IsApplied() {
// Variables have been applied to os.Environ
}Status Queries
LoadTime
func (l *Loader) LoadTime() time.TimeReturns the time of the last file load.
Returns:
time.Time- load time; returns zero value if never loaded
loadTime := loader.LoadTime()
if !loadTime.IsZero() {
fmt.Printf("Last load time: %v\n", loadTime)
}Config
func (l *Loader) Config() ConfigReturns the loader's configuration.
Returns:
Config- configuration (should be treated as read-only)
WARNING
Config() returns a copy of the configuration for read-only use: value fields are isolated copies, but pointer/interface fields such as KeyPattern, FileSystem, and AuditHandler (as well as slice fields like AllowedKeys and RequiredKeys) share their underlying objects with the running loader — mutating them affects loader behavior, so do not modify them. For a safe mutable copy, manually copy the required fields.
cfg := loader.Config()
fmt.Printf("Max file size: %d\n", cfg.MaxFileSize)Validation and Mapping
Validate
func (l *Loader) Validate() errorValidates that required keys exist.
Returns:
error- validation error
Behavior:
- Checks that all keys specified in
ValidationConfig.RequiredKeysexist
cfg := env.DefaultConfig()
cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}
loader, _ := env.New(cfg)
loader.LoadFiles(".env")
if err := loader.Validate(); err != nil {
// Missing required keys
var missingErr *env.ValidationError
if errors.As(err, &missingErr) {
fmt.Printf("Missing: %s\n", missingErr.Field)
}
}ParseInto
func (l *Loader) ParseInto(v any) errorMaps environment variables to a struct.
Parameters:
v- struct pointer
Returns:
error- mapping error
Supported tags:
env:"KEY"- specifies the environment variable nameenv:"-"- ignore this fieldenvDefault:"value"- specifies a default value
Slice fields are separated by comma , by default (spaces around the separator are automatically removed); there is no custom separator tag.
type Config struct {
Host string `env:"HOST" envDefault:"localhost"`
Port int64 `env:"PORT" envDefault:"8080"`
Debug bool `env:"DEBUG" envDefault:"false"`
Hosts []string `env:"HOSTS"`
Ignored string `env:"-"`
}
var cfg Config
err := loader.ParseInto(&cfg)
if err != nil {
panic(err)
}Resource Release
Close
func (l *Loader) Close() errorReleases resources and clears storage.
Returns:
error- close error
Behavior:
- Securely zeros all stored sensitive data
- If the loader owns a ComponentFactory, also closes the factory
- Safe to close; multiple calls return nil
loader, _ := env.New(cfg)
defer loader.Close()
// Use loader...Behavior after Close
After closing, all operations return errors or zero values:
LoadFiles→ErrClosedGetString→ returns empty valueSet→ErrClosedKeys→ returns nilLen→ returns 0
IsClosed
func (l *Loader) IsClosed() boolChecks whether the loader has been closed.
Returns:
bool- whether closed
if loader.IsClosed() {
// loader has been closed
}Complete Example
package main
import (
"errors"
"fmt"
"log"
"os"
"time"
"github.com/cybergodev/env"
)
func main() {
// Create production configuration
cfg := env.ProductionConfig()
cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}
cfg.AuditHandler = env.NewJSONAuditHandler(os.Stdout)
// Create loader
loader, err := env.New(cfg)
if err != nil {
log.Fatal(err)
}
defer loader.Close()
// Load files
if err := loader.LoadFiles(".env", ".env.production"); err != nil {
if errors.Is(err, env.ErrFileNotFound) {
log.Fatal("Configuration file not found")
}
log.Fatal(err)
}
// Validate required keys
if err := loader.Validate(); err != nil {
log.Fatal("Missing required configuration:", err)
}
// Read configuration
host := loader.GetString("DB_HOST")
port := loader.GetInt("DB_PORT", 5432)
debug := loader.GetBool("DEBUG", false)
timeout := loader.GetDuration("TIMEOUT", 30*time.Second)
fmt.Printf("Server: %s:%d\n", host, port)
fmt.Printf("Debug: %v, Timeout: %v\n", debug, timeout)
// Sensitive data
secret := loader.GetSecure("API_KEY")
if secret != nil {
defer secret.Release()
fmt.Printf("API Key length: %d\n", secret.Length())
}
// Apply to system environment
if err := loader.Apply(); err != nil {
log.Fatal(err)
}
// All variables
fmt.Printf("Loaded %d variables\n", loader.Len())
fmt.Printf("Load time: %v\n", loader.LoadTime())
}Lifecycle and Process-Environment Semantics
Close does not roll back os.Environ
Close() zeroes only the in-memory copies of variables; it does not unset variables previously written to the process environment via Apply, AutoApply, or Set (with AutoApply on). To remove them before closing, call Delete per key:
loader.Delete("API_KEY") // unsets in the process env only if this loader set it
loader.Close()Per-key ownership tracking in Delete
The loader records which keys it wrote to the process environment (appliedKeys). Delete unsets only those keys and never touches process variables it did not set (HOME, TERM, ...). Multiple loaders therefore never interfere with each other.
Apply distinguishes "unset" from "explicitly empty"
Apply uses LookupEnv to detect existing values: a variable explicitly set to an empty string counts as "present" and is likewise skipped when OverwriteExisting=false — it is not mistaken for unset and overwritten.
State and metadata methods
| Method | Description |
|---|---|
Config() | Returns a copy of the configuration for read-only use; value fields are copied, but pointer/interface fields such as KeyPattern, FileSystem, and AuditHandler share their underlying objects, and mutating them affects the running loader |
IsClosed() | Whether the loader is closed (nil receiver returns true) |
IsApplied() | Whether variables have been applied to os.Environ |
LoadTime() | Time of the most recent load (zero value if never loaded) |
Key-resolution details on the read path
- Exact match first: lookup tries the key as-is, then falls back to the uppercase form
- Dot paths:
database.hostexpands toDATABASE_HOST(JSON/YAML flattened keys); an indexed path likeservice.cors.origins.0falls back to the comma-separated value when the indexed key is missing - No trimming:
Lookupreturns the stored value as-is —.envvalues were trimmed at parse time, JSON/YAML values may keep surrounding whitespace - Lock-free fast path: single-key reads (
Lookup/GetSecure) bypass the loader-level read lock via an atomic closed flag, avoiding reader-count contention under high concurrency; races withClosedegrade gracefully to "read the value or read the cleared state"
Silent-skip semantics of Set
With OverwriteExisting=false, calling Set on an existing key is silently skipped and returns nil (no error). To observe skips, watch the audit log (skipped (no overwrite) events).
Defense-in-depth for file loading (TOCTOU)
Between the Stat() size check and the actual read there is a theoretical replacement window; the library mitigates it with two layers: parsers internally wrap the file stream in a hardened read path enforcing the size cap, so even a file that grows after the check is cut off at the hard cap (100 MB), preventing memory exhaustion.
Related Documentation
- Package Functions - Package-level convenience functions
- Config API - Configuration options
- SecureValue API - Secure value handling
- Interfaces - All interface definitions