Documentation
¶
Overview ¶
Package env loads environment variables into Go structs, with first-class support for live reload.
The mental model is encoding/json: a struct gets populated from a set of named values via tags, with extension points for custom types, sources, and lifecycle hooks. The goal is the smallest possible top-level surface for the 80% case (Load) and a principled extension story for everything else.
One-line happy path ¶
type Config struct {
Port int `env:"PORT,default=8080"`
DBURL string `env:"DATABASE_URL,required"`
Timeout time.Duration `env:"TIMEOUT,default=30s"`
Hosts []string `env:"HOSTS,separator=;"`
APIKey string `env:"API_KEY,required,secret"`
}
var cfg Config
if err := env.Load(&cfg); err != nil {
log.Fatal(env.FormatError(err))
}
Sources ¶
All lookups go through the Source interface. The OS environment is just one source; `.env` files (via the separate github.com/go-rotini/dotenv package), in-memory maps (Map), source chains (Multi), and remote secret backends (in their own modules) are equal citizens.
loader := env.New(
env.WithSource(env.OSEnv()),
env.WithPrefix("APP_"),
)
Live reload ¶
NewLive wraps the loader in a typed handle that swaps the configuration atomically when an underlying Source reports a change. Reads are O(1) and lock-free via sync/atomic.Pointer; readers always observe a complete, validated snapshot.
live, err := env.NewLive[Config](env.WithSource(env.OSEnv()))
if err != nil { log.Fatal(err) }
defer live.Close()
cfg := live.Get() // *Config — never nil after NewLive succeeds
Related packages ¶
This package has no third-party runtime dependencies and no sub-packages. Adjacent functionality lives in its own module so consumers pay only for what they import:
- github.com/go-rotini/dotenv — `.env` file parser, AST, lossless round-trip, Lint. Its NewSource returns a value whose Lookup method is structurally compatible with Source, so a parsed `.env` file plugs into a loader's source chain — dotenv does not import this package (or anything else; it has no dependencies).
- github.com/go-rotini/fs — filesystem helpers including a content-agnostic file watcher (atomic-rename aware, debounced). Compose it with dotenv (or a yaml/toml/jsonc parser) into a Source that also implements Watcher to drive live reload of config files through NewLive.
Tag system ¶
A single comma-separated tag, mirroring encoding/json:
`env:"NAME,opt1,opt2"`
Supported options: required, notEmpty, default=value, separator=char, kvSeparator=char, expand, fromFile, secret, immutable, layout=format, base64, hex, unset. (`expand` performs `$VAR` / `${VAR}` / `${VAR-default}` / `${VAR:-default}` / `${VAR:?message}` substitution on the resolved value, resolving names verbatim against the source chain; unset references expand to "". `${VAR:?msg}` for an unset VAR fails with ErrExpansionAssertion. As with os.Expand/POSIX shells, inside `${...}` a `-` (or `:-`, `:?`) starts the operator section, so a variable whose name contains `-` cannot be referenced with the `${...}` form — use a name without `-`, or `$NAME` only if no `-` follows. `WithPrefix` is NOT applied to expansion references.) Companion tags: envDesc:"..." for free-text help and envPrefix:"PFX_" on nested struct fields. See the package README for the full table.
Pitfalls ¶
Environment variables come with platform-specific quirks. The list below covers the issues most likely to bite a new user.
OS-level:
- Windows folds env-var names to uppercase; POSIX is case-sensitive. The OSEnv source matches the platform — for cross-platform tests, use Map with WithCaseInsensitive.
- os.Getenv collapses unset and empty into "". The Source interface preserves the distinction via the second bool return; OSEnv uses os.LookupEnv to honor it. The `required` tag option is satisfied by a set-but-empty value; `notEmpty` requires both set and non-empty.
- os.Setenv / os.Unsetenv are not thread-safe under cgo (golang/go #63567). This package never calls them from library code; the `unset` tag option is the single opt-in exception, serialized within a Load call. Disable globally via WithoutUnset in test environments.
- os.Environ returns degenerate entries on Windows (empty keys, leading-`=` per-drive variables). OSEnv filters them; you don't see them via Source.Lookup.
Live reload:
- Reload is driven by any Source that also implements Watcher. Sources that don't are polled at the interval set by WithPollInterval, or read once and never re-read if that option is absent. For file-backed config (`.env` / yaml / toml / jsonc), wrap the parser in a Source + Watcher built on github.com/go-rotini/fs's file watcher, which handles the editor "atomic save" pattern (write-temp-then-rename) by watching the parent directory and filtering by filename — the only correct strategy, since the rewritten file is a new inode.
- The reload pipeline parses + validates into a fresh candidate before swapping; failures retain the previous value and emit an error event. The struct returned by Live.Get is therefore always a successfully-validated snapshot.
- Treat the struct returned by Live.Get as IMMUTABLE. The whole point of the sync/atomic.Pointer swap is that readers can rely on a stable view; mutating it through the returned pointer is a race.
- Fields tagged `immutable` reject reload candidates that would change them. Initial load is exempt; subsequent reloads compare against the last successful snapshot. For a field that is also `expand`-tagged the comparison uses the post-expansion value, so a change in a referenced variable counts even when the field's own `${...}` template is unchanged.
Concurrency:
- Loader is safe for concurrent use after construction.
- Loader.Watch mutates v in place from the worker goroutine — readers must NOT touch v concurrently. For lock-free reads use NewLive instead.
- Source implementations are required to make Lookup safe for concurrent use; built-in sources do.
Test isolation:
- Global os.Environ is shared state; tests that set it are NOT parallel-safe even via testing.T.SetEnv. For parallelizable tests, prefer Map over OSEnv as the loader's source.
Encoding (reverse direction):
- Encode is a tooling feature, not a primary API. Secret-tagged fields redact to "***" by default; WithEncodeIncludeSecrets opts in. Encode + Load is byte-stable for canonical inputs but is not a round-trip for `.env` FILES (comments and original quoting are lost). For lossless `.env` editing, use github.com/go-rotini/dotenv's Parse → (*File).Set → (*File).Marshal.
Architecture:
- This package has zero third-party runtime dependencies and no sub-packages. `.env` parsing is in github.com/go-rotini/dotenv (also dependency-free); file watching is in github.com/go-rotini/fs. Consumers who only need OSEnv + Map pay no transitive cost. Verified by `go list -deps`.
Index ¶
- Constants
- Variables
- func Encode(w io.Writer, v any, opts ...EncodeOption) error
- func EncodeFile(path string, v any, opts ...EncodeOption) error
- func FormatError(err error) string
- func FormatErrorColor(err error) string
- func Load(v any) error
- func LoadTo[T any](opts ...Option) (T, error)
- func Markdown(w io.Writer, v any, opts ...Option) error
- func MustLoad(v any)
- func MustLoadTo[T any](opts ...Option) T
- func PrintUsage(w io.Writer, v any, opts ...Option) error
- type Change
- type CycleError
- type EmptyValueError
- type EncodeOption
- func WithCustomMarshaler[T any](fn func(T) (string, error)) EncodeOption
- func WithEncodeComments(c map[string]string) EncodeOption
- func WithEncodeExport(b bool) EncodeOption
- func WithEncodeIncludeSecrets(b bool) EncodeOption
- func WithEncodeQuoteAll(b bool) EncodeOption
- func WithEncodeSort(b bool) EncodeOption
- type Event
- type FieldDoc
- type ImmutableChangedError
- type Live
- type Loader
- func (l *Loader) Describe(v any) []FieldDoc
- func (l *Loader) Encode(w io.Writer, v any, opts ...EncodeOption) error
- func (l *Loader) EncodeFile(path string, v any, opts ...EncodeOption) error
- func (l *Loader) Load(v any) error
- func (l *Loader) LoadContext(ctx context.Context, v any) error
- func (l *Loader) Markdown(w io.Writer, v any) error
- func (l *Loader) PrintUsage(w io.Writer, v any) error
- func (l *Loader) Snapshot(v any) (map[string]string, error)
- func (l *Loader) Watch(ctx context.Context, v any) (<-chan Event, error)
- type MapOption
- type Marshaler
- type MissingRequiredError
- type MultiError
- type Mutator
- type MutatorFunc
- type Option
- func WithCustomDecoder[T any](fn func(s string, dst *T) error) Option
- func WithFieldNameTransform(fn func(goFieldName string) string) Option
- func WithLogger(l *slog.Logger) Option
- func WithMutator(m ...Mutator) Option
- func WithPollInterval(d time.Duration) Option
- func WithPrefix(prefix string) Option
- func WithReloadDebounce(d time.Duration) Option
- func WithRequireAll() Option
- func WithSecretRedactor(fn func(value string) string) Option
- func WithSource(srcs ...Source) Option
- func WithSourceAppend(src Source) Option
- func WithSourcePrepend(src Source) Option
- func WithTagName(name string) Option
- func WithoutUnset() Option
- type ParseError
- type Secret
- type Source
- type Unmarshaler
- type UnmarshalerContext
- type ValidationError
- type Validator
- type ValidatorContext
- type Watcher
Constants ¶
const DefaultReloadDebounce = 75 * time.Millisecond
DefaultReloadDebounce is the quiet period Loader.Watch and Live wait after the last Source-change event before reloading. 75ms covers an editor save's burst of fsnotify events without feeling laggy in interactive use.
Variables ¶
var ( // ErrMissingRequired matches any [*MissingRequiredError]. ErrMissingRequired = &MissingRequiredError{} // ErrEmptyValue matches any [*EmptyValueError]. ErrEmptyValue = &EmptyValueError{} // ErrParse matches any [*ParseError]. ErrParse = &ParseError{} // ErrValidation matches any [*ValidationError]. ErrValidation = &ValidationError{} // ErrImmutableChanged matches any [*ImmutableChangedError]. ErrImmutableChanged = &ImmutableChangedError{} // ErrCycle matches any [*CycleError]. ErrCycle = &CycleError{} // ErrMultiError matches any [*MultiError]. ErrMultiError = &MultiError{} // ErrNilPointer is returned when [Loader.Load] is called with a nil // pointer or a non-pointer. ErrNilPointer = errors.New("env: non-nil pointer to struct required") // ErrNotStruct is returned when the target type is not a struct. ErrNotStruct = errors.New("env: target must be a struct or pointer to struct") // ErrUnknownTagOption is returned when a struct tag contains an // unrecognized option. ErrUnknownTagOption = errors.New("env: unknown tag option") // ErrConflictingFields is returned when two fields in the same // struct resolve to the same env-var key after prefix accumulation. ErrConflictingFields = errors.New("env: conflicting field names") // ErrUnsupportedType is returned when a struct field's target type // cannot be decoded from a string by any built-in or interface // mechanism (chan, func, complex, bare any, struct without any // unmarshaler). ErrUnsupportedType = errors.New("env: unsupported target type") // ErrExpansionAssertion is returned (wrapped, with field context) // when a `${VAR:?message}` reference in an `expand`-tagged value // names a variable that is not set. ErrExpansionAssertion = errors.New("env: ${VAR:?...} assertion failed") // ErrFromFileRead is returned when a fromFile-tagged field // references a path that cannot be read. ErrFromFileRead = errors.New("env: fromFile target is not readable") )
Sentinel errors. The struct-pointer sentinels match by type via each error's Is method; the errors.New sentinels match by value identity.
var ErrNilContext = errors.New("env: nil context")
ErrNilContext is returned when a watch / load call receives a nil context.
var ErrWatcherClosed = errors.New("env: watcher closed")
ErrWatcherClosed is returned by Live.Reload when the worker goroutine has already exited.
Functions ¶
func Encode ¶
func Encode(w io.Writer, v any, opts ...EncodeOption) error
Encode writes v to w as a `.env`-format byte stream. Each exported, env-tagged field becomes one line: `KEY=VALUE`. Nil pointer fields and nil slice/map fields are omitted.
v may be a struct value or a non-nil pointer to one. Output is sourceable by Bash (with WithEncodeExport) and re-parseable by the github.com/go-rotini/dotenv parser.
Encode uses the default field-name mapping (SCREAMING_SNAKE_CASE), the default tag name ("env"), and no key prefix. For a loader built with WithTagName, WithFieldNameTransform, or WithPrefix, use Loader.Encode so the output round-trips through that loader.
func EncodeFile ¶
func EncodeFile(path string, v any, opts ...EncodeOption) error
EncodeFile encodes v to path with mode 0600 (owner-only — these files often hold secrets). Existing files are truncated.
func FormatError ¶
FormatError renders err into a human-readable string. For *MultiError, each wrapped error is rendered on its own line. Returns "" when err is nil. See FormatErrorColor for an ANSI-colored variant.
func FormatErrorColor ¶
FormatErrorColor is like FormatError but wraps each message in ANSI bold-red. Use it only when the destination is a terminal.
func Load ¶
Load decodes environment variables into v using the default Loader (OS env source, no prefix, "env" tag name). v must be a non-nil pointer to a struct. Per-field errors are aggregated into a *MultiError; field-collection errors (conflicting names, unknown tag options) are returned directly.
func LoadTo ¶
LoadTo is the generic form of Load. T must be a struct type. opts are applied to a fresh Loader; with no opts the default loader is used.
func Markdown ¶
Markdown writes a GitHub-Flavored Markdown table of v's loadable fields to w. Suitable for embedding in `docs/CONFIG.md`.
func MustLoadTo ¶
MustLoadTo is like LoadTo but panics on error.
Types ¶
type Change ¶
type Change struct {
Keys []string
}
Change describes a Source-level change. Keys is the set of variable names that were added, modified, or removed since the last event. An empty Keys slice means "the source signaled a change but did not identify which keys"; the loader treats this as "everything may have changed" and re-reads all fields on the next reload.
type CycleError ¶
type CycleError struct {
// Chain is the reference chain leading to the cycle, e.g.
// ["DSN", "HOST", "DSN"]. It may hold a single element when the
// non-termination is a self-referential default rather than a
// genuine cross-variable cycle.
Chain []string
}
CycleError is reported when variable expansion (the `expand` tag option) does not terminate — a reference cycle (A → B → A) or excessive nesting in `${VAR:-default}` chains.
func (*CycleError) Error ¶
func (e *CycleError) Error() string
func (*CycleError) Is ¶
func (*CycleError) Is(target error) bool
type EmptyValueError ¶
EmptyValueError is reported for a field marked notEmpty whose key is set to the empty string.
func (*EmptyValueError) Error ¶
func (e *EmptyValueError) Error() string
func (*EmptyValueError) Is ¶
func (*EmptyValueError) Is(target error) bool
type EncodeOption ¶
type EncodeOption func(*encoderOptions)
EncodeOption configures Encode and EncodeFile.
func WithCustomMarshaler ¶
func WithCustomMarshaler[T any](fn func(T) (string, error)) EncodeOption
WithCustomMarshaler registers a custom marshaler for type T. Runs at the top of the encoder priority chain, ahead of Marshaler, encoding.TextMarshaler, encoding.BinaryMarshaler, and the built-in registry.
func WithEncodeComments ¶
func WithEncodeComments(c map[string]string) EncodeOption
WithEncodeComments registers per-key comment overrides. Map keys are resolved env-var names; values are emitted as `# <text>` lines preceding the assignment, replacing any envDesc text.
func WithEncodeExport ¶
func WithEncodeExport(b bool) EncodeOption
WithEncodeExport prepends `export ` to each emitted line, producing output sourceable by Bash.
func WithEncodeIncludeSecrets ¶
func WithEncodeIncludeSecrets(b bool) EncodeOption
WithEncodeIncludeSecrets emits the underlying value of secret-tagged fields instead of the `***` marker. Use only when the destination is known to be safe (e.g., piping to a child process via inherited env, not when writing to a public file).
func WithEncodeQuoteAll ¶
func WithEncodeQuoteAll(b bool) EncodeOption
WithEncodeQuoteAll forces every value to be double-quoted. The default emits values bare when they contain no characters that require quoting.
func WithEncodeSort ¶
func WithEncodeSort(b bool) EncodeOption
WithEncodeSort emits keys in lexicographic order; the default is declaration order.
type Event ¶
type Event struct {
// Changed lists the env-var names whose resolved values differ
// from the snapshot taken before this reload, sorted and
// deduplicated. Empty on a failed or no-op reload.
Changed []string
// Err is non-nil on failed reload; the previously-loaded value is
// retained.
Err error
// Time is the wall-clock time the reload completed.
Time time.Time
}
Event is delivered on the channel returned by Loader.Watch or by Live.Events. One Event corresponds to one reload attempt — successful or failed.
type FieldDoc ¶
type FieldDoc struct {
// Name is the resolved env-var name, with [WithPrefix] applied.
Name string
// GoType is the destination type in source notation, e.g.
// "int", "time.Duration", "[]string", "*url.URL".
GoType string
// Description is the free-text help string from the envDesc tag.
Description string
// Default is the literal default value from the default= tag option.
// Empty when no default is set.
Default string
// Required is true when the field carries the required tag option,
// or implicitly required under [WithRequireAll].
Required bool
// NotEmpty is true when the field carries the notEmpty tag option.
NotEmpty bool
// Secret is true when the field carries the secret tag option.
Secret bool
// Immutable is true when the field carries the immutable tag option.
Immutable bool
// Separator is the slice/array separator from the separator= tag
// option, or empty when defaulting to ",".
Separator string
}
FieldDoc describes one loadable field. Returned by Describe and consumed by PrintUsage / Markdown; users may also walk the slice programmatically (for instance to render their own help layout).
type ImmutableChangedError ¶
ImmutableChangedError is reported when a reload candidate would have changed a field tagged immutable. For secret-tagged fields, Old and New are redacted (run through the loader's secret redactor).
func (*ImmutableChangedError) Error ¶
func (e *ImmutableChangedError) Error() string
func (*ImmutableChangedError) Is ¶
func (*ImmutableChangedError) Is(target error) bool
type Live ¶
type Live[T any] struct { // contains filtered or unexported fields }
Live wraps a Loader in an atomic-pointer-swapping live-reload container. After NewLive returns, Live.Get is lock-free and torn-read-free: every reload installs a fresh `*T` via atomic.Pointer.Store, so readers always observe a complete, validated snapshot.
Treat the struct returned by Get as IMMUTABLE — mutating it would race with future readers, defeating the atomic-swap design. The intended pattern is "read fields off Get() in a hot path"; for any state the reader itself mutates, copy out first.
func NewLive ¶
NewLive constructs a Loader with the given options and performs an initial Load. Returns nil and the error on first-load failure. On success the returned Live runs a worker goroutine that reloads in response to source-change events and Live.Reload calls.
func NewLiveWithLoader ¶
NewLiveWithLoader is like NewLive but uses an existing *Loader.
func (*Live[T]) Close ¶
Close stops the worker, cancels every Watcher subscription, and closes the events channel. Idempotent. Live.Get continues to return the last-loaded value after Close.
func (*Live[T]) Events ¶
Events returns the channel of Event values from the worker — one per reload attempt (success or failure). The channel is closed by Live.Close.
The channel is buffered (capacity 16); a slow consumer drops surplus events. Get() still returns the freshest value, so dropped events never hide a successful reload from a hot-path reader.
func (*Live[T]) Get ¶
func (l *Live[T]) Get() *T
Get returns the most recently loaded value. Lock-free, torn-read- free, never nil after NewLive succeeds. Callers must treat the pointed-to struct as immutable; the worker may install a new pointer at any time, but every previously-handed-out pointer continues to reference a frozen value.
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader is a configured environment-variable loader. Construct with New; safe for concurrent use after construction.
func New ¶
New returns a Loader with opts applied to the defaults: source chain = [OSEnv], no prefix, tag name "env", SCREAMING_SNAKE_CASE field-name transform, no logger, optional-by-default field policy.
func (*Loader) Describe ¶
Describe is the Loader-method form of Describe. FieldDoc.Name includes the loader's WithPrefix.
func (*Loader) Encode ¶
Encode is the loader-aware counterpart of Encode: it honors this loader's WithTagName, WithFieldNameTransform, and WithPrefix, so the result round-trips through this loader's Loader.Load. Source-, mutator-, validator-, and WithoutUnset-related options play no part in encoding.
func (*Loader) EncodeFile ¶
func (l *Loader) EncodeFile(path string, v any, opts ...EncodeOption) error
EncodeFile is the loader-aware counterpart of EncodeFile; see Loader.Encode.
func (*Loader) Load ¶
Load decodes the configured sources into v. v must be a non-nil pointer to a struct. Per-field errors aggregate into a *MultiError after every field is attempted; field-collection errors (conflicting names, unknown tag options) are returned directly.
func (*Loader) LoadContext ¶
LoadContext is like Loader.Load but accepts a context. ctx is forwarded to the Mutator chain, to fields whose type implements UnmarshalerContext, and to a ValidatorContext hook on the target struct (if it implements one). ctx must not be nil.
func (*Loader) PrintUsage ¶
PrintUsage is the Loader-method form of PrintUsage. Returns an error when v is not a struct, struct tags fail to validate, or writing fails.
func (*Loader) Snapshot ¶
Snapshot returns, for every recognized field of v, the raw value the loader would start from: the source-chain value if present, otherwise the field's default= value; fields with neither are omitted. Keys carry WithPrefix. Mutators, the `expand` tag, `fromFile`, and decoding are NOT applied — Snapshot is the "what the sources say" view, useful for tests and debugging (feed it back to Map).
func (*Loader) Watch ¶
Watch starts live reload. v must be a non-nil pointer to a struct (same constraints as Loader.Load). Watch performs an initial Load synchronously; on failure it returns a nil channel and the error.
On every change pushed by a Watcher source — or on every poll tick when WithPollInterval is set — Watch builds a fresh candidate of v's type, runs the full load pipeline, compares the candidate's immutable-tagged values against the previous reload's snapshot, and on success overwrites *v. One Event is delivered per reload attempt; failures (validation, immutable, decode) deliver an Event with Err set and leave *v unmodified.
The channel is closed when ctx is canceled.
v must NOT be read concurrently with the worker's writes. For lock-free, torn-read-free reads use NewLive instead.
type MapOption ¶
type MapOption func(*mapOptions)
MapOption configures a Map source.
func WithCaseInsensitive ¶
func WithCaseInsensitive() MapOption
WithCaseInsensitive returns a MapOption that makes the Map source fold ASCII keys to uppercase for comparison, mirroring Windows env-var semantics. Useful in cross-platform tests that need to exercise the Windows case-folding behavior on a POSIX host.
Folding is ASCII-only; non-ASCII bytes pass through unchanged.
type Marshaler ¶
Marshaler is implemented by types that render themselves as a string for the reverse Encode path.
type MissingRequiredError ¶
MissingRequiredError is reported for a field marked required (or notEmpty) whose key is absent from every Source and which has no default.
func (*MissingRequiredError) Error ¶
func (e *MissingRequiredError) Error() string
func (*MissingRequiredError) Is ¶
func (*MissingRequiredError) Is(target error) bool
type MultiError ¶
type MultiError struct {
Errors []error
}
MultiError aggregates per-field errors from a single Load call. Implements the Go 1.20+ Unwrap() []error convention so errors.Is and errors.As walk every branch.
func (*MultiError) Append ¶
func (m *MultiError) Append(err error)
Append appends err to the aggregator. Nil errors are ignored.
func (*MultiError) Error ¶
func (m *MultiError) Error() string
func (*MultiError) Is ¶
func (*MultiError) Is(target error) bool
type Mutator ¶
type Mutator interface {
// MutateEnv inspects or rewrites the value the loader is about to
// decode.
//
// originalValue : the source's view (no defaults applied);
// "" when sourced is false.
// currentValue : the value after default application and any
// earlier mutators in the chain.
// sourced : false when no Source returned ok=true.
//
// stop=true halts the chain immediately and feeds newValue to the
// decoder. A non-nil err terminates the per-field pipeline.
MutateEnv(
ctx context.Context,
originalKey, resolvedKey, originalValue, currentValue string,
sourced bool,
) (newValue string, stop bool, err error)
}
Mutator observes or rewrites resolved values before they reach the decoder. The chain runs once per field per Load, after source lookup and default application, before decoding. Common uses: resolving indirections (e.g., "secret://path" → vault fetch), synthesizing values for unset fields, observation for logging or metrics.
Mutators do NOT run for fields tagged env:"-". They DO run for unset fields with no default — a mutator may synthesize a value.
type MutatorFunc ¶
type MutatorFunc func( ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string, sourced bool, ) (string, bool, error)
MutatorFunc adapts a function into a Mutator.
type Option ¶
type Option func(*loaderOptions)
Option configures a Loader. Apply via New or LoadTo.
func WithCustomDecoder ¶
WithCustomDecoder registers a custom decoder for type T. Runs at the top of the decoder priority chain, ahead of Unmarshaler, encoding.TextUnmarshaler, encoding.BinaryUnmarshaler, and the built-in registry. fn receives the resolved string and a pointer to the destination of type T.
func WithFieldNameTransform ¶
WithFieldNameTransform overrides the default SCREAMING_SNAKE_CASE transform for fields without an explicit tag name. fn receives the Go field name and returns the env-var name.
Loaders with a custom transform bypass the per-type field-info cache and re-walk the struct on every Load.
func WithLogger ¶
WithLogger directs non-fatal loader diagnostics to l. Default is a no-op logger.
func WithMutator ¶
WithMutator registers one or more [Mutator]s in chain order. May be called multiple times to extend the chain. The chain runs once per field, after source lookup and default application, before decoding.
func WithPollInterval ¶
WithPollInterval configures how often Loader.Watch and Live re-read sources that do not implement Watcher. Default 0 (no polling); a positive duration enables poll-based reload for non-Watcher sources. Watcher sources push events regardless. Negative values are clamped to 0.
func WithPrefix ¶
WithPrefix prepends prefix to every resolved env-var name. Applied before any tag-supplied envPrefix on nested struct fields.
func WithReloadDebounce ¶
WithReloadDebounce coalesces burst events from Watcher sources: the worker waits for d of quiet before re-running the load pipeline; events arriving within d collapse into one reload. Default DefaultReloadDebounce. Negative values are clamped to 0 (no debouncing — intended for tests only).
func WithRequireAll ¶
func WithRequireAll() Option
WithRequireAll inverts the default optional-by-default policy: every untagged field becomes implicitly required unless it has a default= tag option.
func WithSecretRedactor ¶
WithSecretRedactor overrides the redaction function applied to secret-tagged field values surfacing in error messages and Describe / PrintUsage / Markdown output. The default returns "<redacted: N bytes>" (or "<redacted>" for empty). Pass nil to restore the default. The encoder's separate "***" marker is unaffected.
func WithSource ¶
WithSource replaces the loader's source chain. The default chain is OSEnv alone; to layer additional sources, list them explicitly:
env.WithSource(env.OSEnv(), dotenvSrc)
Sources are consulted in order; the first to return ok=true wins. An empty argument list produces a loader whose chain has no entries.
func WithSourceAppend ¶
WithSourceAppend adds src to the end of the existing source chain (lowest precedence — consulted only when no earlier source matches).
func WithSourcePrepend ¶
WithSourcePrepend adds src to the front of the existing source chain (highest precedence).
func WithTagName ¶
WithTagName overrides the struct tag name the loader recognizes (default "env"). Set to "envconfig" for caarlos0/envconfig-style migrations. Does not affect the envDesc / envPrefix companion tags.
Loaders with a non-default tag name bypass the per-type field-info cache and re-walk the struct on every Load.
func WithoutUnset ¶
func WithoutUnset() Option
WithoutUnset disables the per-field `unset` tag option globally, keeping the process env inspectable after Load. Useful in tests.
type ParseError ¶
ParseError is reported when a string value cannot be converted to a field's target Go type. Field, Key, Value, and Type may be empty when the error originates inside the decoder before the loader has wrapped it with field context.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Is ¶
func (*ParseError) Is(target error) bool
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type Secret ¶
type Secret[T any] struct { // contains filtered or unexported fields }
Secret is a generic wrapper that hides T's value in textual output and integrates with the standard observability surfaces:
- fmt.Stringer, fmt.GoStringer: return the redaction marker, so fmt.Println / %v / %#v emit the marker, never the value.
- slog.LogValuer: emits the marker, so slog attributes redact automatically.
- Marshaler: returns the underlying T for the reverse Encode path. The encoder's secret-tag check normally redacts to "***" first; MarshalEnv runs only under WithEncodeIncludeSecrets.
- Unmarshaler: decodes via the standard decoder pipeline; works for any T the decoder accepts.
The wrapper and the secret tag option are interchangeable for redaction purposes:
type Config struct {
Token env.Secret[string] `env:"TOKEN,required"`
Other string `env:"OTHER,required,secret"`
}
func (Secret[T]) GoString ¶
GoString implements fmt.GoStringer and returns the redaction marker, so %#v formatting also redacts.
func (Secret[T]) LogValue ¶
LogValue implements slog.LogValuer and returns the redaction marker.
func (Secret[T]) MarshalEnv ¶
MarshalEnv implements Marshaler for the reverse Encode path. Renders the inner T via fmt.Sprint — round-trip stable for primitives; other types may need a custom marshaler.
func (Secret[T]) MarshalJSON ¶
MarshalJSON implements encoding/json.Marshaler and returns the redaction marker as a JSON string.
func (Secret[T]) String ¶
String implements fmt.Stringer and returns the redaction marker.
func (*Secret[T]) UnmarshalEnv ¶
UnmarshalEnv implements Unmarshaler by delegating to the package's decoder against the inner T. If T relies on UnmarshalerContext, it receives context.Background() (the wrapper has no context to thread).
type Source ¶
type Source interface {
// Lookup returns the value associated with key and whether the key is
// set. ok=true with an empty value distinguishes "set but empty" from
// "unset"; this distinction is preserved end-to-end through the loader.
Lookup(key string) (value string, ok bool)
}
Source provides string-keyed value lookups. It is the primary contract by which the loader resolves env-var values: the OS environment is just one source, and `.env` files, in-memory maps, and remote secret backends all implement the same interface.
Implementations MUST make Lookup safe for concurrent use. All built-in sources satisfy this; third-party sources should as well.
func Map ¶
Map returns a Source backed by a copy of m. Mutating m after construction does not affect the Source. The returned Source is safe for concurrent Lookup.
Pass WithCaseInsensitive to enable Windows-style ASCII case-insensitive key matching.
func Multi ¶
Multi returns a Source that consults srcs in order; the first source returning ok=true wins. An empty src list returns a Source whose Lookup always returns ("", false). A single-source argument is returned as-is without an additional indirection.
func OSEnv ¶
func OSEnv() Source
OSEnv returns a Source backed by os.LookupEnv. The source is stateless and reflects the live process environment on every Lookup.
On Windows, os.LookupEnv folds variable names to uppercase for comparison; on POSIX systems names are case-sensitive.
Lookup rejects degenerate keys (empty, leading "=", containing "=" or NUL) without consulting the OS, so callers passing such keys always observe ok=false instead of triggering platform-specific edge cases like Windows "=C:" per-drive hidden variables.
type Unmarshaler ¶
Unmarshaler is implemented by types that parse themselves from a string. The decoder calls UnmarshalEnv on the field's address, so types should declare the method with a pointer receiver.
type UnmarshalerContext ¶
UnmarshalerContext is like Unmarshaler but receives the context passed to Loader.LoadContext (or context.Background() for Loader.Load / the package-level Load). The loader checks a field's address for this interface and, failing that, for Unmarshaler — a type satisfies one or the other, never both (Go method names cannot be overloaded). It is consulted only at the top level of a field, not for slice elements or map values, which the (context-free) value decoder handles.
type ValidationError ¶
type ValidationError struct {
Cause error
}
ValidationError wraps an error returned from a target struct's Validator.Validate (or ValidatorContext.Validate) method.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Is ¶
func (*ValidationError) Is(target error) bool
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
type Validator ¶
type Validator interface {
Validate() error
}
Validator is implemented by struct types that need cross-field validation after loading. The loader detects it via interface assertion on the target's pointer receiver and calls Validate once, after every field has been populated. If per-field errors exist Validate is skipped, since the struct is in a partial state. A non-nil return is wrapped in a *ValidationError and added to the *MultiError.
type Config struct {
DBHost string `env:"DB_HOST"`
DBUser string `env:"DB_USER"`
}
func (c *Config) Validate() error {
if c.DBHost != "" && c.DBUser == "" {
return fmt.Errorf("DB_USER is required when DB_HOST is set")
}
return nil
}
type ValidatorContext ¶
ValidatorContext is like Validator but receives the context passed to Loader.LoadContext (or context.Background() for Loader.Load). The loader uses whichever of ValidatorContext / Validator the target struct implements — it can only be one (Go method names cannot be overloaded).
type Watcher ¶
type Watcher interface {
// Watch returns a channel that delivers Change events as the source's
// underlying state changes. The returned channel MUST be closed when
// ctx is canceled or when the source determines it can no longer
// produce events (for example, the underlying file is deleted with
// no re-bind possible). Each call to Watch returns an independent
// subscription.
Watch(ctx context.Context) (<-chan Change, error)
}
Watcher is optionally implemented by sources that can push change notifications. Sources that don't implement Watcher are polled at the interval set by WithPollInterval when used under Loader.Watch or Live; without that option, non-Watcher sources are read once at the initial load and never re-read.