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:
- Operating system environment (via os.LookupEnv)
- 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 ¶
- Variables
- func CountRecordedEvents() int
- func EnableDedupKeys(enable bool)
- func EnableRecording(enable bool)
- func EnableStackTraces(enable bool)
- func IsRecording() bool
- func LoadEnvFile(path string) (map[string]string, error)
- func Many(ctx context.Context, keys ...string) map[string]Reader[string]
- func RegisterObserver(obs Observer) func()
- func SetEnvOverride(key string, value string, set func(any, any))
- func SetEnvOverrides(values map[string]string, set func(any, any))
- func Split2[A any, B any](value Reader[tuple.Tuple2[A, B]]) (Reader[A], Reader[B])
- func Split3[A any, B any, C any](value Reader[tuple.Tuple3[A, B, C]]) (Reader[A], Reader[B], Reader[C])
- 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])
- 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])
- 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])
- func WithEnvOverride(ctx context.Context, key string, value string) context.Context
- func WithEnvOverrides(ctx context.Context, values map[string]string) context.Context
- type Loader
- func (l *Loader) AsMap() map[string]string
- func (l *Loader) AsSlice() []string
- func (l *Loader) Clear()
- func (l *Loader) Contains(key string) bool
- func (l *Loader) Delete(key string)
- func (l *Loader) EnhanceContext(ctx context.Context) context.Context
- func (l *Loader) Filter(predicate func(key string, value string) (keep bool))
- func (l *Loader) Get(key string) (string, bool)
- func (l *Loader) Keys() []string
- func (l *Loader) LoadEnv()
- func (l *Loader) LoadFile(filename string) (int64, error)
- func (l *Loader) Set(key string, value string)
- func (l *Loader) SetAll(env map[string]string)
- func (l *Loader) SetContext(setter func(any, any))
- type Observer
- type Option
- type Reader
- func Bool(ctx context.Context, key string, opts ...Option[bool]) Reader[bool]
- func Bytes(ctx context.Context, key string, opts ...Option[[]byte]) Reader[[]byte]
- func Combine2[A any, B any](first Reader[A], second Reader[B]) Reader[tuple.Tuple2[A, B]]
- func Combine3[A any, B any, C any](first Reader[A], second Reader[B], third Reader[C]) Reader[tuple.Tuple3[A, B, C]]
- 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]]
- func Combine5[A any, B any, C any, D any, E any](first Reader[A], second Reader[B], third Reader[C], fourth Reader[D], ...) Reader[tuple.Tuple5[A, B, C, D, E]]
- 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], ...) Reader[tuple.Tuple6[A, B, C, D, E, F]]
- func Duration(ctx context.Context, key string, opts ...Option[time.Duration]) Reader[time.Duration]
- func FileContents(ctx context.Context, key string, opts ...Option[[]byte]) Reader[[]byte]
- func FilePath(ctx context.Context, key string, opts ...Option[envtypes.LocalPath]) Reader[envtypes.LocalPath]
- func Float32(ctx context.Context, key string, opts ...Option[float32]) Reader[float32]
- func Float64(ctx context.Context, key string, opts ...Option[float64]) Reader[float64]
- func GzipLevel(ctx context.Context, key string, opts ...Option[int]) Reader[int]
- func HostAndPort(ctx context.Context, key string, opts ...Option[envtypes.HostPort]) Reader[envtypes.HostPort]
- func Int[I xform.Intish](ctx context.Context, key string, opts ...Option[I]) Reader[I]
- func Map[A any, B any](env Reader[A], f func(A) (B, error)) Reader[B]
- func NewReader[T any](key string, present bool, err error, value T) Reader[T]
- func NoValue[T any]() Reader[T]
- func Port(ctx context.Context, key string, opts ...Option[uint16]) Reader[uint16]
- func SlogLevel(ctx context.Context, key string, opts ...Option[slog.Level]) Reader[slog.Level]
- func String(ctx context.Context, key string, opts ...Option[string]) Reader[string]
- func String2(ctx context.Context, key1 string, key2 string, opts ...Option[string]) Reader[tuple.Tuple2[string, string]]
- func String3(ctx context.Context, key1 string, key2 string, key3 string, ...) Reader[tuple.Tuple3[string, string, string]]
- func String4(ctx context.Context, key1 string, key2 string, key3 string, key4 string, ...) Reader[tuple.Tuple4[string, string, string, string]]
- func String5(ctx context.Context, key1 string, key2 string, key3 string, key4 string, ...) Reader[tuple.Tuple5[string, string, string, string, string]]
- func String6(ctx context.Context, key1 string, key2 string, key3 string, key4 string, ...) Reader[tuple.Tuple6[string, string, string, string, string, string]]
- func Time(ctx context.Context, key string, format string, opts ...Option[time.Time]) Reader[time.Time]
- func URL(ctx context.Context, key string, opts ...Option[*url.URL]) Reader[*url.URL]
- func UUID(ctx context.Context, key string, opts ...Option[uuid.UUID]) Reader[uuid.UUID]
- func Uint[U xform.Uintish](ctx context.Context, key string, opts ...Option[U]) Reader[U]
- func VarMap(ctx context.Context, keys ...string) Reader[map[string]string]
- func (e Reader[A]) DoWithValue(f func(A))
- func (e Reader[A]) Error() error
- func (e Reader[A]) HasError() bool
- func (e Reader[A]) HasValue() bool
- func (e Reader[A]) Key() string
- func (e Reader[A]) Map(f func(A) (A, error)) Reader[A]
- func (e Reader[A]) String() string
- func (e Reader[A]) Value() (A, error)
- func (e Reader[A]) ValueOrElse(v A) A
- func (e Reader[A]) ValueOrElseFunc(f func() A) A
- func (e Reader[A]) ValueOrElseFuncErr(f func() (A, error)) (A, error)
- func (e Reader[A]) ValueOrFatal() A
- func (e Reader[A]) ValueOrPanic() A
- func (e Reader[A]) WithDefault(v A) Reader[A]
- func (e Reader[A]) WithErrorIfMissing(err error) Reader[A]
- func (e Reader[A]) WithFallback(v Reader[A]) Reader[A]
- type Source
- type ValueReadEvent
Constants ¶
This section is empty.
Variables ¶
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") )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
Loading from files: loader := envutil.NewLoader() loader.LoadFile(".env") // Load base configuration loader.LoadFile(".env.production") // Override with environment-specific values
Programmatic manipulation: loader.Set("DATABASE_URL", "postgres://localhost/test") loader.Delete("LEGACY_CONFIG") if loader.Contains("FEATURE_FLAG") { // feature is enabled }
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()
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
- Check context overrides (from this loader)
- Check actual process environment (os.Getenv)
- Use default value if provided
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 Fallback ¶
Fallback allows you to provide a fallback Reader to use if the Reader is missing a value.
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 ¶
Bool returns a Reader for a boolean environment variable. Accepts: "true", "false", "1", "0", "yes", "no" (case-insensitive).
func Combine2 ¶
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 ¶
Duration returns a Reader for a time.Duration environment variable. Accepts formats like "300ms", "1.5h", "2h45m".
func FileContents ¶
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 GzipLevel ¶
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 ¶
Int returns a Reader for an integer environment variable. Supports all signed integer types: int, int8, int16, int32, int64.
func Map ¶
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 ¶
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 ¶
NoValue returns an empty Reader with no value. Useful as a placeholder or when constructing Readers programmatically.
func SlogLevel ¶
SlogLevel returns a Reader for a slog.Level environment variable. Accepts: "debug", "info", "warn", "error" (case-insensitive).
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 UUID ¶
UUID returns a Reader for a UUID environment variable. Accepts standard UUID format: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".
func Uint ¶
Uint returns a Reader for an unsigned integer environment variable. Supports all unsigned integer types: uint, uint8, uint16, uint32, uint64.
func VarMap ¶
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]) Map ¶
Map transforms the value using f, preserving the same type. Convenience wrapper around the package-level Map function.
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 ¶
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 ¶
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 ¶
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 ¶
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)
}