envutil

package
v0.0.0-...-5103540 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 28, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package envutil provides type-safe environment variable parsing with a fluent API. It offers built-in support for strings, integers, booleans, durations, URLs, UUIDs, file paths, and more, with optional defaults, validation, and error handling.

Environment variables can be read from two sources:

  1. Operating system environment (via os.LookupEnv)
  2. Context-based overrides (via WithEnvOverride) - useful for testing and multi-tenancy

Recording and Observation: The package supports optional recording and observation of environment variable reads for debugging, auditing, and testing purposes:

  • EnableRecording() enables collection of ValueReadEvent records
  • RegisterObserver() allows real-time notification when variables are read
  • Each event includes the key, value, source (Environment/Context/None), and optional stack trace

Note: This package documentation consolidates information from multiple files (envutil.go and record.go).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBadEnvVar is returned when an environment variable cannot be parsed.
	ErrBadEnvVar = errors.New("error parsing environment variable")
	// ErrEnvVarMissing is returned when a required environment variable is not set.
	ErrEnvVarMissing = errors.New("missing environment variable")
)
View Source
var ErrUnknownFileType = errors.New("env file doesn't have a known file suffix")

ErrUnknownFileType is returned when the file extension is not recognized.

Functions

func CountRecordedEvents

func CountRecordedEvents() int

CountRecordedEvents returns the number of recorded environment variable read events. Thread-safe - acquires mutex to safely read the events slice length.

func EnableDedupKeys

func EnableDedupKeys(enable bool)

EnableDedupKeys controls whether duplicate keys are filtered from recording. When enabled, only the first read of each environment variable key is recorded. Subsequent reads of the same key are silently ignored.

This is useful for reducing noise in audit logs when the same environment variables are read multiple times during application startup.

This setting only affects recording - observers are still notified for all reads.

func EnableRecording

func EnableRecording(enable bool)

EnableRecording controls whether environment variable reads are recorded. When enabled, each read operation creates a ValueReadEvent that can be retrieved later.

Parameters:

  • enable: Whether to record environment variable reads.
  • includeStacks: Whether to capture stack traces for each read (adds overhead).

This is typically used in testing or debugging to track which environment variables are accessed and where in the code they're read from.

func EnableStackTraces

func EnableStackTraces(enable bool)

EnableStackTraces controls whether stack traces are captured for each environment variable read. Stack traces are useful for debugging to see where reads occur, but they add performance overhead.

This setting only affects reads when recording is enabled via EnableRecording. Can be toggled independently at runtime without affecting the recording state.

func IsRecording

func IsRecording() bool

IsRecording returns whether environment variable recording is currently enabled. Thread-safe - uses atomic load operation.

func LoadEnvFile

func LoadEnvFile(path string) (map[string]string, error)

LoadEnvFile loads environment variables from a file and returns them as a map. The file format is automatically detected based on the file extension:

  • .env files are parsed as key=value pairs (one per line)
  • .json files are expected to have an "env" field containing string key-value pairs
  • .yml/.yaml files are expected to have an "env" field containing string key-value pairs

Example usage:

vars, err := LoadEnvFile("/path/to/.env")
if err != nil {
    return err
}
for key, value := range vars {
    os.Setenv(key, value)
}

Returns an error if:

  • The file doesn't exist or can't be read
  • The file extension is not recognized (.env, .json, .yml, .yaml)
  • The file content cannot be parsed according to its format

func Many

func Many(ctx context.Context, keys ...string) map[string]Reader[string]

Many returns a map of Readers for multiple environment variable keys. Useful when you need to process several related variables with individual error handling.

Example:

readers := Many(ctx, "DATABASE_URL", "API_KEY", "PORT")
dbURL, _ := readers["DATABASE_URL"].Value()
apiKey, _ := readers["API_KEY"].Value()
port, _ := readers["PORT"].Map(xform.Int).Value()

For a simpler map of values (without individual Readers), see VarMap.

func RegisterObserver

func RegisterObserver(obs Observer) func()

RegisterObserver registers a callback function that will be invoked immediately whenever an environment variable is read (if recording is enabled).

Observers are called synchronously during environment variable reads, so they should execute quickly to avoid blocking. For expensive operations, consider using a buffered channel or goroutine within your observer.

Returns an unregister function that removes the observer when called. The unregister function is safe to call multiple times.

Example usage:

unregister := RegisterObserver(func(event ValueReadEvent) {
    log.Printf("Read %s=%s from %s", event.Key, event.Value, event.Source)
})
defer unregister() // Clean up when done

EnableRecording(true, false)
// ... code that reads environment variables ...
// Observer is called immediately for each read

func SetEnvOverride

func SetEnvOverride(key string, value string, set func(any, any))

SetEnvOverride configures a single environment variable override using a callback setter function. This is used with lazy value overrides to set environment variable overrides without directly manipulating a context. The set function is typically provided by lazy override mechanisms (e.g., lazy.SetValueOverride) to store the value for later retrieval.

Parameters:

  • key: The environment variable name to override
  • value: The override value to use instead of the actual environment variable
  • set: Callback function that stores the key-value pair. If nil, the function returns early.

This function is typically used in conjunction with lazy value override systems where context values need to be configured before a context is created.

func SetEnvOverrides

func SetEnvOverrides(values map[string]string, set func(any, any))

SetEnvOverrides configures multiple environment variable overrides using a callback setter function. This is a more efficient version of calling SetEnvOverride multiple times. The set function is typically provided by lazy override mechanisms to store each key-value pair for later retrieval.

Parameters:

  • values: Map of environment variable names to override values
  • set: Callback function that stores each key-value pair. If nil, the function returns early.

This function is typically used in conjunction with lazy value override systems where context values need to be configured before a context is created.

func Split2

func Split2[A any, B any](
	value Reader[tuple.Tuple2[A, B]],
) (Reader[A], Reader[B])

Split2 splits a Reader[Tuple2] into 2 separate Readers. If the input Reader has an error or is missing, both output Readers will too.

func Split3

func Split3[A any, B any, C any](
	value Reader[tuple.Tuple3[A, B, C]],
) (Reader[A], Reader[B], Reader[C])

Split3 splits a Reader[Tuple3] into 3 separate Readers. If the input Reader has an error or is missing, all output Readers will too.

func Split4

func Split4[A any, B any, C any, D any](
	value Reader[tuple.Tuple4[A, B, C, D]],
) (Reader[A], Reader[B], Reader[C], Reader[D])

Split4 splits a Reader[Tuple4] into 4 separate Readers. If the input Reader has an error or is missing, all output Readers will too.

func Split5

func Split5[A any, B any, C any, D any, E any](
	value Reader[tuple.Tuple5[A, B, C, D, E]],
) (Reader[A], Reader[B], Reader[C], Reader[D], Reader[E])

Split5 splits a Reader[Tuple5] into 5 separate Readers. If the input Reader has an error or is missing, all output Readers will too.

func Split6

func Split6[A any, B any, C any, D any, E any, F any](
	value Reader[tuple.Tuple6[A, B, C, D, E, F]],
) (Reader[A], Reader[B], Reader[C], Reader[D], Reader[E], Reader[F])

Split6 splits a Reader[Tuple6] into 6 separate Readers. If the input Reader has an error or is missing, all output Readers will too.

func WithEnvOverride

func WithEnvOverride(ctx context.Context, key string, value string) context.Context

WithEnvOverride returns a new context with a single environment variable override. This allows you to override environment variables for a specific operation without modifying the actual process environment.

When envutil Reader functions (String, Int, Bool, etc.) are called with this context, they will first check for overrides in the context before reading from the actual environment.

Example:

ctx := envutil.WithEnvOverride(context.Background(), "PORT", "9090")
port := envutil.IntCtx(ctx, "PORT", envutil.Default(8080)).Value()
// port will be 9090, even if PORT=8080 in the actual environment

This is particularly useful for:

  • Testing: Override environment variables without affecting other tests
  • Request-scoped configuration: Different handlers can use different values
  • Multi-tenant scenarios: Different tenants can have different configuration

func WithEnvOverrides

func WithEnvOverrides(ctx context.Context, values map[string]string) context.Context

WithEnvOverrides returns a new context with multiple environment variable overrides. This is a more efficient version of calling WithEnvOverride multiple times, as it stores all overrides in a single context operation.

If the provided values map is empty, the original context is returned unchanged to avoid unnecessary context allocations.

Example:

ctx := envutil.WithEnvOverrides(context.Background(), map[string]string{
	"DATABASE_URL": "postgres://localhost/test",
	"PORT":         "9090",
	"LOG_LEVEL":    "debug",
})
// All envutil Reader functions will check these overrides first

This is particularly useful for:

  • Testing: Set up a complete test environment in one call
  • Configuration injection: Pass environment-specific config through context
  • Batch operations: Override multiple settings for a group of operations

Types

type Loader

type Loader struct {
	// contains filtered or unexported fields
}

Loader provides an isolated, mutable collection of environment variables. Unlike directly modifying os.Setenv, a Loader maintains its own environment map that can be manipulated independently of the process environment.

The Loader supports multiple workflows:

  1. Loading from files: loader := envutil.NewLoader() loader.LoadFile(".env") // Load base configuration loader.LoadFile(".env.production") // Override with environment-specific values

  2. Programmatic manipulation: loader.Set("DATABASE_URL", "postgres://localhost/test") loader.Delete("LEGACY_CONFIG") if loader.Contains("FEATURE_FLAG") { // feature is enabled }

  3. Context integration: ctx := loader.EnhanceContext(context.Background()) // All envutil Reader functions will check this context for overrides port := envutil.IntCtx(ctx, "PORT", envutil.Default(8080)).Value()

  4. Exporting to other systems: cmd := exec.Command("my-app") cmd.Env = loader.AsSlice() // Pass environment to subprocess

Thread-safety: Loader is NOT thread-safe. If you need concurrent access, you must synchronize access using a mutex or create separate Loader instances.

The loader maintains an internal map of environment variables that can be modified using Set, Delete, and Load. The loader does not modify the actual process environment (os.Setenv is never called).

func NewLoader

func NewLoader() *Loader

NewLoader creates a new empty Loader with no environment variables. The loader starts with an empty environment map, allowing you to build up your configuration from scratch by loading files or setting values programmatically.

Unlike the previous behavior, NewLoader does NOT automatically load the process environment. Use LoadEnv() if you want to include the current process environment, or use LoadFile() to load from configuration files.

Example usage:

// Create an empty loader
ldr := envutil.NewLoader()

// Option 1: Load from files only (no process env)
ldr.LoadFile(".env")
ldr.LoadFile(".env.production")

// Option 2: Start with process env, then layer files
ldr.LoadEnv()  // Load current process environment first
ldr.LoadFile(".env.local")  // Override with local config

// Option 3: Build environment programmatically
ldr.Set("PORT", "8080")
ldr.Set("DATABASE_URL", "postgres://localhost/test")

// Use the environment in a context
ctx := ldr.EnhanceContext(context.Background())

Common use cases:

  • Testing: Build isolated test environments without process env pollution
  • Configuration layering: Explicitly control which sources to load and their priority
  • Clean slate: Start fresh without inheriting unwanted process variables
  • Programmatic config: Build configuration entirely in code

func (*Loader) AsMap

func (l *Loader) AsMap() map[string]string

AsMap returns a copy of the loader's environment as a map. The returned map is independent of the loader; modifying it won't affect the loader's internal state.

Example usage:

loader := envutil.NewLoader()
loader.LoadFile(".env")

// Get a snapshot of the environment
snapshot := loader.AsMap()

// Pass to a function that expects map[string]string
config := parseConfig(snapshot)

// Create a modified copy without affecting the loader
modified := loader.AsMap()
modified["PORT"] = "9090"  // Doesn't change loader

This is useful when you need:

  • A snapshot of the current environment state
  • To pass environment to functions expecting map[string]string
  • To create a modified copy without affecting the original loader
  • To marshal the environment to JSON/YAML

Returns a new map containing all environment variables.

func (*Loader) AsSlice

func (l *Loader) AsSlice() []string

AsSlice returns the loader's environment as a slice of "KEY=VALUE" strings. This format is compatible with exec.Cmd.Env and other systems that expect environment variables in this format.

The order of elements is not guaranteed (map iteration order is random in Go).

Example usage:

loader := envutil.NewLoader()
loader.LoadFile(".env")
loader.Set("APP_ENV", "production")

// Pass environment to a subprocess
cmd := exec.Command("./worker")
cmd.Env = loader.AsSlice()
if err := cmd.Run(); err != nil {
    return err
}

// Write environment to a file
file, err := os.Create(".env.generated")
if err != nil {
    return err
}
defer file.Close()
for _, line := range loader.AsSlice() {
    fmt.Fprintln(file, line)
}

Common use cases:

  • Passing environment to subprocesses (exec.Cmd.Env)
  • Generating .env files programmatically
  • Exporting configuration for container environments
  • Debugging: Print all variables in a readable format

Returns a new slice of "KEY=VALUE" strings.

func (*Loader) Clear

func (l *Loader) Clear()

Clear removes all environment variables from the loader, resetting it to an empty state. After calling Clear, the loader will contain no environment variables until new ones are loaded or set.

This method does NOT modify the actual process environment. The clearing only affects this loader instance and any contexts created from it.

Example usage:

loader := envutil.NewLoader()
loader.LoadEnv()  // Load process environment
loader.LoadFile(".env")  // Load file configuration

// ... do some work ...

// Clear everything and start fresh
loader.Clear()
loader.LoadFile(".env.test")  // Load test-specific config only

Common use cases:

  • Testing: Reset loader state between test cases
  • Re-initialization: Start fresh without creating a new loader
  • Conditional loading: Clear and reload based on runtime conditions
  • Memory cleanup: Remove all variables when no longer needed

Alternative: If you want to start completely fresh, consider creating a new loader with NewLoader() instead, which may be more explicit and easier to understand.

func (*Loader) Contains

func (l *Loader) Contains(key string) bool

Contains checks if an environment variable exists in the loader. Returns true if the key exists (regardless of value), false otherwise.

Example usage:

if loader.Contains("DEBUG_MODE") {
    // Debug mode is configured (value might be "true", "false", or anything else)
}

if !loader.Contains("REQUIRED_CONFIG") {
    return errors.New("REQUIRED_CONFIG must be set")
}

Note: This only checks for the key's existence, not the value. To check both existence and retrieve the value, use Get:

value, found := loader.Get("PORT")
if found {
    fmt.Printf("PORT is set to: %s\n", value)
}

Returns true if the key exists in the loader's environment, false otherwise.

func (*Loader) Delete

func (l *Loader) Delete(key string)

Delete removes an environment variable from the loader. If the key doesn't exist, this is a no-op (no error is returned).

This method does NOT modify the actual process environment. The deletion only affects this loader instance and any contexts created from it.

Example usage:

loader := envutil.NewLoader()
loader.LoadFile(".env")

// Remove sensitive variables before passing to subprocess
loader.Delete("SECRET_KEY")
loader.Delete("API_TOKEN")

cmd := exec.Command("./worker")
cmd.Env = loader.AsSlice()  // Worker won't have access to secrets

Common use cases:

  • Security: Remove sensitive variables before export
  • Testing: Remove variables to test default behavior
  • Cleanup: Remove legacy or deprecated configuration
  • Isolation: Prevent specific variables from being inherited

func (*Loader) EnhanceContext

func (l *Loader) EnhanceContext(ctx context.Context) context.Context

EnhanceContext creates a new context with all loader environment variables as overrides. This allows envutil Reader functions (String, Int, Bool, etc.) to use the loader's environment instead of the process environment when called with this context.

The enhanced context provides a layered configuration approach:

  1. Check context overrides (from this loader)
  2. Check actual process environment (os.Getenv)
  3. Use default value if provided
  4. Return error if required and not found

Example usage:

// Load configuration from files
loader := envutil.NewLoader()
loader.LoadFile(".env")
loader.LoadFile(".env.test")

// Create context with loader's environment
ctx := loader.EnhanceContext(context.Background())

// All envutil readers will use the loader's environment
port := envutil.IntCtx(ctx, "PORT", envutil.Default(8080)).Value()
dbURL := envutil.StringCtx(ctx, "DATABASE_URL", envutil.Required()).ValueOrFatal()
debug := envutil.BoolCtx(ctx, "DEBUG_MODE", envutil.Default(false)).Value()

Common use cases:

  • Testing: Inject test-specific configuration without modifying process environment
  • Request handlers: Different requests can have different configuration contexts
  • Multi-tenant apps: Each tenant can have isolated configuration
  • Configuration isolation: Run operations with specific config without side effects

Example with HTTP handler:

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    loader := envutil.NewLoader()
    loader.Set("TENANT_ID", r.Header.Get("X-Tenant-ID"))
    loader.Set("REQUEST_ID", r.Header.Get("X-Request-ID"))

    ctx := loader.EnhanceContext(r.Context())
    h.processRequest(ctx)  // Handler uses tenant-specific config
}

Example with testing:

func TestDatabaseConnection(t *testing.T) {
    loader := envutil.NewLoader()
    loader.Set("DATABASE_URL", "postgres://localhost/test")
    loader.Set("DB_POOL_SIZE", "5")

    ctx := loader.EnhanceContext(context.Background())
    db, err := connectDatabase(ctx)  // Uses test database
    require.NoError(t, err)
}

Returns a new context with all loader environment variables as overrides. The original context is not modified.

See WithEnvOverrides for more details on context-based environment overrides.

func (*Loader) Filter

func (l *Loader) Filter(predicate func(key string, value string) (keep bool))

Filter removes environment variables from the loader that don't match the predicate. The predicate function is called for each key-value pair, and only pairs where the predicate returns true are kept. All other variables are removed from the loader.

This is a destructive operation that modifies the loader in-place. Variables that don't match the predicate are permanently removed. This method does NOT modify the actual process environment.

Example usage:

loader := envutil.NewLoader()
loader.LoadEnv()  // Start with all process environment variables

// Keep only variables starting with "APP_"
loader.Filter(func(key, value string) bool {
    return strings.HasPrefix(key, "APP_")
})

// Keep only non-empty values
loader.Filter(func(key, value string) bool {
    return value != ""
})

// Keep specific variables by name
allowedVars := map[string]bool{
    "DATABASE_URL": true,
    "REDIS_URL":    true,
    "PORT":         true,
}
loader.Filter(func(key, value string) bool {
    return allowedVars[key]
})

// Remove sensitive variables before passing to subprocess
loader.Filter(func(key, value string) bool {
    sensitive := []string{"SECRET", "PASSWORD", "TOKEN", "KEY"}
    for _, s := range sensitive {
        if strings.Contains(key, s) {
            return false  // Remove sensitive variables
        }
    }
    return true  // Keep everything else
})

Common use cases:

  • Namespace isolation: Keep only variables with specific prefixes
  • Security: Remove sensitive variables before export
  • Cleanup: Remove empty or invalid values
  • Allowlist/blocklist: Keep or remove specific sets of variables
  • Validation: Remove variables that don't meet certain criteria

Performance note: This method creates a new internal map and replaces the old one, so it's O(n) where n is the number of variables in the loader.

Alternative: If you want to keep the original environment and create a filtered copy, use AsMap() to get a copy first, then create a new loader and filter that.

func (*Loader) Get

func (l *Loader) Get(key string) (string, bool)

Get retrieves the value of an environment variable from the loader. Returns the value and true if the key exists, or an empty string and false if not found.

Example usage:

value, found := loader.Get("DATABASE_URL")
if !found {
    return errors.New("DATABASE_URL not configured")
}
fmt.Printf("Database: %s\n", value)

Note: Unlike os.Getenv, this only checks the loader's internal environment, not the actual process environment. To check both, use the envutil Reader functions with a context created by EnhanceContext:

ctx := loader.EnhanceContext(context.Background())
dbURL := envutil.StringCtx(ctx, "DATABASE_URL", envutil.Required()).ValueOrFatal()

Returns:

  • value: The environment variable value if found, empty string otherwise
  • found: True if the key exists in the loader's environment, false otherwise

func (*Loader) Keys

func (l *Loader) Keys() []string

Keys returns a slice of all environment variable names in the loader. The order of keys is not guaranteed (map iteration order is random in Go).

Example usage:

loader := envutil.NewLoader()
loader.LoadFile(".env")

// Print all configured variables
for _, key := range loader.Keys() {
    value, _ := loader.Get(key)
    fmt.Printf("%s=%s\n", key, value)
}

// Check for required variables
requiredKeys := []string{"DATABASE_URL", "API_KEY", "PORT"}
loadedKeys := loader.Keys()
for _, required := range requiredKeys {
    if !slices.Contains(loadedKeys, required) {
        return fmt.Errorf("missing required variable: %s", required)
    }
}

Returns a new slice containing all environment variable names. The returned slice is a copy; modifying it won't affect the loader.

func (*Loader) LoadEnv

func (l *Loader) LoadEnv()

LoadEnv loads all environment variables from the current process into the loader. This captures a snapshot of the process environment at the time of the call, merging it into the loader's existing environment. Variables from the process environment will override any existing variables with the same key in the loader.

This method is useful when you want to include the process environment as part of your configuration, either as a base layer or to merge with file-based config.

Example usage:

// Start with process environment, then override with files
loader := envutil.NewLoader()
loader.LoadEnv()                    // Base: current process environment
loader.LoadFile(".env.local")       // Override with local config
loader.LoadFile(".env.production")  // Override with production config

// Modify specific values
loader.Set("DEBUG_MODE", "true")

Common use cases:

  • Baseline configuration: Use process env as a starting point
  • Merging sources: Combine process env with file-based config
  • Snapshot isolation: Capture process env at a specific point in time
  • Testing: Load test environment without polluting process env

Important notes:

  • This does NOT modify the actual process environment (os.Setenv is not called)
  • Variables are loaded once at call time; changes to process env afterward won't be reflected
  • If called multiple times, each call will re-snapshot and override existing values
  • Lines without '=' are silently ignored (malformed environment entries)

func (*Loader) LoadFile

func (l *Loader) LoadFile(filename string) (int64, error)

LoadFile reads environment variables from a file and merges them into the loader. The file format is automatically detected based on the file extension:

  • .env files: KEY=VALUE pairs (one per line)
  • .json files: Must have an "env" field with string key-value pairs
  • .yml/.yaml files: Must have an "env" field with string key-value pairs

Variables loaded from the file will override any existing variables with the same key in the loader's environment. This allows you to layer configurations by calling Load multiple times with different files:

loader := envutil.NewLoader()
loader.LoadFile(".env")                 // Base configuration
loader.LoadFile(".env.local")           // Local overrides (higher priority)
loader.LoadFile(".env.production")      // Production-specific overrides (highest priority)

The method returns the number of variables loaded from the file and any error encountered. If the file doesn't exist, can't be read, or has an invalid format, an error is returned and the loader's state remains unchanged.

Example .env file:

# Database configuration
DATABASE_URL=postgres://localhost/myapp
DB_POOL_SIZE=10

Example JSON file:

{
  "env": {
    "DATABASE_URL": "postgres://localhost/myapp",
    "DB_POOL_SIZE": "10"
  }
}

Returns:

  • count: Number of environment variables loaded from the file
  • error: Non-nil if the file cannot be read or parsed

See LoadEnvFile for more details on supported file formats and parsing behavior.

func (*Loader) Set

func (l *Loader) Set(key string, value string)

Set adds or updates an environment variable in the loader. If the key already exists, its value is replaced. If the key doesn't exist, it is added to the loader's environment.

This method does NOT modify the actual process environment (os.Setenv is not called). The change only affects this loader instance and any contexts created from it.

Example usage:

loader := envutil.NewLoader()

// Set configuration values
loader.Set("PORT", "8080")
loader.Set("LOG_LEVEL", "debug")
loader.Set("FEATURE_XYZ_ENABLED", "true")

// Override existing values
loader.Set("DATABASE_URL", "postgres://localhost/test")  // Overrides value from .env

Common use cases:

  • Testing: Override specific variables for test scenarios
  • Dynamic configuration: Set values based on runtime conditions
  • Configuration templating: Start with base config and customize
  • Feature flags: Enable/disable features programmatically

func (*Loader) SetAll

func (l *Loader) SetAll(env map[string]string)

SetAll adds or updates multiple environment variables in the loader from a map. This is a convenience method for setting multiple variables at once, equivalent to calling Set() for each key-value pair in the map.

Variables in the map will override any existing variables with the same key in the loader's environment. This method does NOT modify the actual process environment.

Example usage:

loader := envutil.NewLoader()

// Set multiple configuration values at once
config := map[string]string{
    "DATABASE_URL":    "postgres://localhost/myapp",
    "REDIS_URL":       "redis://localhost:6379",
    "PORT":            "8080",
    "LOG_LEVEL":       "debug",
    "FEATURE_X_ENABLED": "true",
}
loader.SetAll(config)

// Merge with existing environment
loader.LoadEnv()  // Load process environment first
overrides := map[string]string{
    "DATABASE_URL": "postgres://localhost/test",  // Override for testing
    "LOG_LEVEL":    "error",                      // Override log level
}
loader.SetAll(overrides)

Common use cases:

  • Bulk configuration: Set multiple related variables at once
  • Testing: Apply a complete test configuration in one call
  • Merging configs: Combine configurations from different sources
  • Dynamic configuration: Apply runtime-computed configuration maps

Note: The order of iteration over the map is not guaranteed (Go map iteration is random). If you need deterministic ordering, set variables individually using Set() in a specific order.

func (*Loader) SetContext

func (l *Loader) SetContext(setter func(any, any))

SetContext configures environment variable overrides using a callback setter function. This is used with lazy value override systems where context values need to be configured before a context is created.

The setter function is typically provided by lazy initialization mechanisms (e.g., lazy.SetValueOverride) that store key-value pairs for later retrieval when the actual context is created.

Parameters:

  • setter: Callback function that stores each key-value pair from the loader's environment. If nil, this method returns immediately without effect.

Example usage with lazy initialization:

// Configure lazy context with loader's environment
loader := envutil.NewLoader()
loader.LoadFile(".env")
loader.LoadFile(".env.production")

// Set overrides using lazy mechanism
lazyCtx := lazy.NewContext()
loader.SetContext(lazyCtx.SetValueOverride)

// Later, when context is materialized, it will have loader's environment
ctx := lazyCtx.Get()
// envutil readers will use the loader's environment
port := envutil.IntCtx(ctx, "PORT", envutil.Default(8080)).Value()

Advanced example - custom setter:

// Custom setter that filters sensitive variables
sensitiveKeys := map[string]bool{"API_KEY": true, "SECRET": true}
loader.SetContext(func(key, value any) {
    keyStr := key.(string)
    if !sensitiveKeys[keyStr] {
        myContextBuilder.Set(key, value)
    }
})

This method is less commonly used than EnhanceContext. Use EnhanceContext when:

  • You have a context and want to add loader's environment to it
  • You're working with standard context.Context

Use SetContext when:

  • Working with lazy initialization frameworks
  • You need to configure overrides before context creation
  • Integrating with custom context management systems

See SetEnvOverrides for more details on the callback-based override mechanism.

type Observer

type Observer func(ValueReadEvent)

Observer is a callback function that gets invoked immediately when an environment variable is read (if recording is enabled). The function receives a ValueReadEvent containing details about the read operation.

Observers are called synchronously during the environment variable read, so they should execute quickly to avoid blocking the caller.

type Option

type Option[T any] func(Reader[T]) Reader[T]

Option is a function which modifies a Reader. It's used by functions like String and Bool so that the caller can easily provide defaults, missing errors, fallbacks, and validation.

func Default

func Default[T any](dfl T) Option[T]

Default allows you to provide a default value for the Reader.

func Fallback

func Fallback[T any](f Reader[T]) Option[T]

Fallback allows you to provide a fallback Reader to use if the Reader is missing a value.

func IfMissing

func IfMissing[T any](err error) Option[T]

IfMissing allows you to provide an error to return if the Reader is missing a value.

func Validate

func Validate[T any](f func(T) error) Option[T]

Validate allows you to provide a validation function to run on the Reader's value. If the validation function returns an error, the Reader will return that error.

type Reader

type Reader[A any] struct {
	// contains filtered or unexported fields
}

Reader represents a parsed environment variable value with error handling. It provides a fluent API for working with environment variables, including type conversions, defaults, validation, and graceful error handling.

func Bool

func Bool(ctx context.Context, key string, opts ...Option[bool]) Reader[bool]

Bool returns a Reader for a boolean environment variable. Accepts: "true", "false", "1", "0", "yes", "no" (case-insensitive).

func Bytes

func Bytes(ctx context.Context, key string, opts ...Option[[]byte]) Reader[[]byte]

Bytes returns a Reader for a byte slice environment variable.

func Combine2

func Combine2[A any, B any](
	first Reader[A],
	second Reader[B],
) Reader[tuple.Tuple2[A, B]]

Combine2 combines 2 Readers into a single Reader containing a Tuple2. All-or-nothing: if any Reader has an error or is missing, the result will too.

func Combine3

func Combine3[A any, B any, C any](
	first Reader[A],
	second Reader[B],
	third Reader[C],
) Reader[tuple.Tuple3[A, B, C]]

Combine3 combines 3 Readers into a single Reader containing a Tuple3. All-or-nothing: if any Reader has an error or is missing, the result will too.

func Combine4

func Combine4[A any, B any, C any, D any](
	first Reader[A],
	second Reader[B],
	third Reader[C],
	fourth Reader[D],
) Reader[tuple.Tuple4[A, B, C, D]]

Combine4 combines 4 Readers into a single Reader containing a Tuple4. All-or-nothing: if any Reader has an error or is missing, the result will too.

func Combine5

func Combine5[A any, B any, C any, D any, E any](
	first Reader[A],
	second Reader[B],
	third Reader[C],
	fourth Reader[D],
	fifth Reader[E],
) Reader[tuple.Tuple5[A, B, C, D, E]]

Combine5 combines 5 Readers into a single Reader containing a Tuple5. All-or-nothing: if any Reader has an error or is missing, the result will too.

func Combine6

func Combine6[A any, B any, C any, D any, E any, F any](
	first Reader[A],
	second Reader[B],
	third Reader[C],
	fourth Reader[D],
	fifth Reader[E],
	sixth Reader[F],
) Reader[tuple.Tuple6[A, B, C, D, E, F]]

Combine6 combines 6 Readers into a single Reader containing a Tuple6. All-or-nothing: if any Reader has an error or is missing, the result will too.

func Duration

func Duration(ctx context.Context, key string, opts ...Option[time.Duration]) Reader[time.Duration]

Duration returns a Reader for a time.Duration environment variable. Accepts formats like "300ms", "1.5h", "2h45m".

func FileContents

func FileContents(ctx context.Context, key string, opts ...Option[[]byte]) Reader[[]byte]

FileContents returns a Reader that reads file contents from a path. The environment variable value is treated as a file path, which is read into memory. Note: Default() provides default file contents, not a default file path.

Example:

// CONFIG_PATH=/etc/app/config.json
contents, _ := FileContents(ctx, "CONFIG_PATH").Value()
// contents = []byte containing the file data from /etc/app/config.json
//
// If CONFIG_PATH is not set or points to non-existent file, Value() returns an error
// unless you provide a Default with fallback contents

func FilePath

func FilePath(ctx context.Context, key string, opts ...Option[envtypes.LocalPath]) Reader[envtypes.LocalPath]

FilePath returns a Reader for a file path environment variable. Validates that the path points to an existing file (not a directory).

func Float32

func Float32(ctx context.Context, key string, opts ...Option[float32]) Reader[float32]

Float32 returns a Reader for a float32 environment variable.

func Float64

func Float64(ctx context.Context, key string, opts ...Option[float64]) Reader[float64]

Float64 returns a Reader for a float64 environment variable.

func GzipLevel

func GzipLevel(ctx context.Context, key string, opts ...Option[int]) Reader[int]

GzipLevel returns a Reader for a gzip compression level environment variable. Valid values: gzip.DefaultCompression, gzip.BestSpeed, gzip.BestCompression, gzip.NoCompression, gzip.HuffmanOnly.

Example:

level, _ := GzipLevel(ctx, "COMPRESSION_LEVEL", Default(gzip.DefaultCompression)).Value()

func HostAndPort

func HostAndPort(ctx context.Context, key string, opts ...Option[envtypes.HostPort]) Reader[envtypes.HostPort]

HostAndPort returns a Reader for a host:port environment variable. Expected format: "hostname:port" (e.g., "localhost:8080", "db.example.com:5432").

func Int

func Int[I xform.Intish](ctx context.Context, key string, opts ...Option[I]) Reader[I]

Int returns a Reader for an integer environment variable. Supports all signed integer types: int, int8, int16, int32, int64.

func Map

func Map[A any, B any](env Reader[A], f func(A) (B, error)) Reader[B]

Map transforms a Reader's value from type A to type B using function f. Returns a new Reader with the transformed value, preserving errors and missing state.

func NewReader

func NewReader[T any](key string, present bool, err error, value T) Reader[T]

NewReader creates a Reader from raw values instead of environment variables. Useful when you want Reader's fluent API and error handling but with data from a different source.

func NoValue

func NoValue[T any]() Reader[T]

NoValue returns an empty Reader with no value. Useful as a placeholder or when constructing Readers programmatically.

func Port

func Port(ctx context.Context, key string, opts ...Option[uint16]) Reader[uint16]

Port returns a Reader for a network port environment variable. Valid range: 1-65535.

func SlogLevel

func SlogLevel(ctx context.Context, key string, opts ...Option[slog.Level]) Reader[slog.Level]

SlogLevel returns a Reader for a slog.Level environment variable. Accepts: "debug", "info", "warn", "error" (case-insensitive).

func String

func String(ctx context.Context, key string, opts ...Option[string]) Reader[string]

String returns a Reader for the given environment variable key.

func String2

func String2(
	ctx context.Context,
	key1 string,
	key2 string,
	opts ...Option[string],
) Reader[tuple.Tuple2[string, string]]

String2 returns a Reader containing a tuple of 2 environment variables. All-or-nothing: if any variable is missing, the entire Reader is missing. For more flexibility, use individual Readers or VarMap.

func String3

func String3(
	ctx context.Context,
	key1 string,
	key2 string,
	key3 string,
	opts ...Option[string],
) Reader[tuple.Tuple3[string, string, string]]

String3 returns a Reader containing a tuple of 3 environment variables. All-or-nothing: if any variable is missing, the entire Reader is missing. For more flexibility, use individual Readers or VarMap.

func String4

func String4(
	ctx context.Context,
	key1 string,
	key2 string,
	key3 string,
	key4 string,
	opts ...Option[string],
) Reader[tuple.Tuple4[string, string, string, string]]

String4 returns a Reader containing a tuple of 4 environment variables. All-or-nothing: if any variable is missing, the entire Reader is missing. For more flexibility, use individual Readers or VarMap.

func String5

func String5(
	ctx context.Context,
	key1 string,
	key2 string,
	key3 string,
	key4 string,
	key5 string,
	opts ...Option[string],
) Reader[tuple.Tuple5[string, string, string, string, string]]

String5 returns a Reader containing a tuple of 5 environment variables. All-or-nothing: if any variable is missing, the entire Reader is missing. For more flexibility, use individual Readers or VarMap.

func String6

func String6(
	ctx context.Context,
	key1 string,
	key2 string,
	key3 string,
	key4 string,
	key5 string,
	key6 string,
	opts ...Option[string],
) Reader[tuple.Tuple6[string, string, string, string, string, string]]

String6 returns a Reader containing a tuple of 6 environment variables. All-or-nothing: if any variable is missing, the entire Reader is missing. For more flexibility, use individual Readers or VarMap.

func Time

func Time(ctx context.Context, key string, format string, opts ...Option[time.Time]) Reader[time.Time]

Time returns a Reader for a time.Time environment variable. The format parameter specifies the expected time format (e.g., time.RFC3339).

func URL

func URL(ctx context.Context, key string, opts ...Option[*url.URL]) Reader[*url.URL]

URL returns a Reader for a URL environment variable. Parses the value using url.Parse.

func UUID

func UUID(ctx context.Context, key string, opts ...Option[uuid.UUID]) Reader[uuid.UUID]

UUID returns a Reader for a UUID environment variable. Accepts standard UUID format: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".

func Uint

func Uint[U xform.Uintish](ctx context.Context, key string, opts ...Option[U]) Reader[U]

Uint returns a Reader for an unsigned integer environment variable. Supports all unsigned integer types: uint, uint8, uint16, uint32, uint64.

func VarMap

func VarMap(ctx context.Context, keys ...string) Reader[map[string]string]

VarMap returns a Reader containing a map of environment variable values. Only variables that are present in the environment are included in the map. All variables are treated as optional; missing variables are simply omitted.

Example:

// Request DATABASE_URL and API_KEY, but only DATABASE_URL is set
vars, _ := VarMap(ctx, "DATABASE_URL", "API_KEY").Value()
// Result: map[string]string{"DATABASE_URL": "postgres://..."}
// API_KEY is not in the map because it wasn't set

func (Reader[A]) DoWithValue

func (e Reader[A]) DoWithValue(f func(A))

DoWithValue calls f with the value if present and valid, otherwise does nothing.

func (Reader[A]) Error

func (e Reader[A]) Error() error

Error returns the parsing error, if any.

func (Reader[A]) HasError

func (e Reader[A]) HasError() bool

HasError returns true if a parsing error occurred.

func (Reader[A]) HasValue

func (e Reader[A]) HasValue() bool

HasValue returns true if the variable is present and valid.

func (Reader[A]) Key

func (e Reader[A]) Key() string

Key returns the environment variable name.

func (Reader[A]) Map

func (e Reader[A]) Map(f func(A) (A, error)) Reader[A]

Map transforms the value using f, preserving the same type. Convenience wrapper around the package-level Map function.

func (Reader[A]) String

func (e Reader[A]) String() string

String returns a string representation of the Reader for debugging.

func (Reader[A]) Value

func (e Reader[A]) Value() (A, error)

Value returns the parsed value or an error if missing or invalid.

func (Reader[A]) ValueOrElse

func (e Reader[A]) ValueOrElse(v A) A

ValueOrElse returns the value or a default if missing or invalid. Logs a warning if there was a parsing error.

func (Reader[A]) ValueOrElseFunc

func (e Reader[A]) ValueOrElseFunc(f func() A) A

ValueOrElseFunc returns the value or calls f() if missing or invalid. Useful when the fallback value is expensive to compute.

func (Reader[A]) ValueOrElseFuncErr

func (e Reader[A]) ValueOrElseFuncErr(f func() (A, error)) (A, error)

ValueOrElseFuncErr returns the value or calls f() if missing or invalid. Like ValueOrElseFunc but allows the fallback function to return an error.

func (Reader[A]) ValueOrFatal

func (e Reader[A]) ValueOrFatal() A

ValueOrFatal returns the value or exits the program (os.Exit(1)) if missing or invalid.

func (Reader[A]) ValueOrPanic

func (e Reader[A]) ValueOrPanic() A

ValueOrPanic returns the value or panics if missing or invalid.

func (Reader[A]) WithDefault

func (e Reader[A]) WithDefault(v A) Reader[A]

WithDefault returns a new Reader with v as the value if missing. If the value is present, returns the Reader unchanged.

func (Reader[A]) WithErrorIfMissing

func (e Reader[A]) WithErrorIfMissing(err error) Reader[A]

WithErrorIfMissing returns a new Reader with err if the value is missing. If the value is present or already has an error, returns the Reader unchanged.

func (Reader[A]) WithFallback

func (e Reader[A]) WithFallback(v Reader[A]) Reader[A]

WithFallback returns v if this Reader has no value, otherwise returns this Reader.

type Source

type Source string

Source indicates where an environment variable value originated from. This helps distinguish between different sources of configuration values.

const (
	// None indicates the environment variable was not set in any source.
	None Source = "none"

	// Environment indicates the value came from the operating system environment.
	Environment Source = "environment"

	// Context indicates the value came from a context.Context value.
	Context Source = "context"

	// File indicates that the value came from an external file.
	File Source = "file"
)

type ValueReadEvent

type ValueReadEvent struct {
	// Time is when the environment variable was read.
	Time time.Time `json:"time"`

	// Key is the environment variable name (e.g., "PORT", "DATABASE_URL").
	Key string `json:"key"`

	// Value is the raw string value of the environment variable.
	// Omitted from JSON if empty.
	Value string `json:"value,omitempty"`

	// IsSet indicates whether the environment variable was actually set.
	// False means the variable was not found and a default may have been used.
	IsSet bool `json:"is_set"`

	// Source indicates where the value came from (environment, context, or none).
	Source Source `json:"source"`

	// Stack contains the call stack trace showing where the read occurred.
	// Only populated if stack traces are enabled via EnableRecording.
	// Omitted from JSON if empty.
	Stack []byte `json:"stack,omitempty"`
}

ValueReadEvent represents a single environment variable read operation. It captures the key, value, source, timestamp, and optionally the call stack.

func CollectRecordingEvents

func CollectRecordingEvents(shouldClear bool) []ValueReadEvent

CollectRecordingEvents returns a copy of all recorded environment variable events. Optionally clears the internal event buffer after copying.

Parameters:

  • shouldClear: If true, the internal events buffer is cleared after copying.

Returns a new slice containing copies of all recorded events. Thread-safe - acquires mutex during the copy operation.

Example usage:

EnableRecording(true, false)
// ... code that reads environment variables ...
events := CollectRecordingEvents(true) // Get events and clear buffer
for _, event := range events {
    fmt.Printf("%s: %s from %s\n", event.Key, event.Value, event.Source)
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL