Documentation
¶
Overview ¶
Package envx provides typed environment-variable reading with injectable lookup for production Go services.
An Env reads variables through a configurable lookup function (defaults to os.LookupEnv) and converts them to typed values via generic Bind. All unresolved required variables and parse failures are collected into a single joined error on Env.Validate.
env := envx.New(envx.WithPrefix("APP"))
port := envx.Bind(env, "PORT", 8080)
host := envx.Bind(env, "DB_HOST", "localhost")
secret := envx.BindRequired[string](env, "SECRET")
if err := env.Validate(); err != nil {
log.Fatal(err) // APP_SECRET is required
}
fmt.Println(port.Value()) // 8080 or from APP_PORT
fmt.Println(secret.Value()) // from APP_SECRET
Supported types ¶
Bind supports string, bool, int, int32, int64, uint, float64, exact time.Duration (ParseDuration, unit required), time.Time and named types convertible to it (RFC3339), and []string (comma-separated, whitespace-trimmed), plus defined types whose underlying kind is a supported builtin (named int64 parses as an integer, not a duration), and types whose pointer implements encoding.TextUnmarshaler.
Overlaying onto a config struct (cfgx → envx → clix) ¶
envx is the environment layer of the precedence pipeline. Use BindTo to overlay variables onto a struct already populated by cfgx, then let clix flags override on top — all through plain pointer sharing:
cfgx.Load("config.yaml", &cfg) // file layer
port := envx.BindTo(env, "PORT", &cfg.Port) // env overrides file
clix.AddFlag(port.Ptr(), "port", ...) // flag overrides env
envx imports no other urx subpackage; the layers compose via pointers.
Testing ¶
Inject a custom lookup to avoid touching the real environment:
env := envx.New(
envx.WithPrefix("APP"),
envx.WithLookup(envx.MapLookup(map[string]string{
"APP_PORT": "9090",
"APP_SECRET": "test-key",
})),
)
Zero dependencies ¶
envx depends only on the Go standard library.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrMissing is reported by [Env.Validate] for a variable bound with // [BindRequired] or [BindRequiredTo] that was not present in the // environment. Safe to compare with == or [errors.Is]. ErrMissing = errors.New("envx: required environment variable not set") // ErrInvalid is reported by [Env.Validate] when a variable was present // but its value could not be parsed into the requested type. Safe to // compare with == or [errors.Is]. ErrInvalid = errors.New("envx: invalid environment variable value") )
Functions ¶
func BindField ¶ added in v1.5.2
BindField overlays one Field from Walk onto env using the same lookup (prefix + fallbacks), parse, default-keep, and Env.Validate path as BindTo. It does not return a typed Var: clix flag aliasing stays on BindTo / Var.Ptr.
Panics if env or f.Ptr is nil — programming errors, matching BindTo.
func MapLookup ¶
MapLookup returns a lookup function backed by a static map. Useful for testing without touching the real environment.
func Walk ¶ added in v1.5.2
func Walk(dst any, opts ...WalkOption) iter.Seq[Field]
Walk yields bindable exported leaves of dst. It does not read the environment and does not mutate dst: a nil pointer field is skipped, not allocated. dst must be a non-nil pointer to struct — a programming error otherwise, matching BindTo.
Default key source is KeysFromEnvTag (allowlist). Slice and map fields are not descended except a leaf []string (or string-element slice). Two fields that alias the same pointer are walked once: the second path is skipped (cycle / diamond termination). An interface holding a non-pointer struct value yields no leaves: the boxed value is not addressable, and Walk does not allocate a copy (BindField would write the copy, not the original). Named types convertible to time.Time are not Walk leaves (*T does not implement encoding.TextUnmarshaler); use Bind/BindTo.
Example ¶
ExampleWalk binds tagged fields through Walk + BindField. Bind remains the canonical overlay; Walk is opt-in reflection with an env-tag allowlist.
package main
import (
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
type Config struct {
Port int `env:"PORT"`
Host string `env:"HOST"`
}
cfg := Config{Port: 8080, Host: "localhost"}
env := envx.New(envx.WithLookup(envx.MapLookup(map[string]string{
"PORT": "9090",
})))
for f := range envx.Walk(&cfg) {
envx.BindField(env, f)
}
if err := env.Validate(); err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%s:%d\n", cfg.Host, cfg.Port)
}
Output: localhost:9090
Types ¶
type Env ¶
type Env struct {
// contains filtered or unexported fields
}
Env holds configuration and tracks bound variables. Create with New. An Env is not safe for concurrent Bind calls; build it on one goroutine during startup, then read the resulting Var values freely.
func New ¶
New creates an Env with the given options. Default configuration: no prefix, lookup os.LookupEnv. Nil options are ignored.
func (*Env) Validate ¶
Validate checks every bound variable: required variables must be present and all present values must have parsed successfully. Returns a single joined error describing all failures, or nil when every binding is valid.
Errors wrap ErrMissing (required but absent) or ErrInvalid (present but unparseable); use errors.Is to distinguish them.
type Field ¶ added in v1.5.2
type Field struct {
Key string // relative name before prefix: "SERVER_PORT"
Path string // Go path: "Server.Port"
Ptr any // *T on the field; never nil for a yielded field
}
Field is one bindable leaf yielded by Walk. Ptr is always a non-nil pointer to the field (*T). Walk never writes through Ptr; the caller decides whether to BindField.
type Option ¶
type Option func(*config)
Option configures New behavior.
func WithFallbackPrefix ¶ added in v1.5.2
WithFallbackPrefix appends a prefix tried only when the primary (WithPrefix) key is unset. Multiple calls append in try order after the primary. Normalization matches WithPrefix: upper-case, trailing "_" trimmed. First-fill-wins: a found primary is never overwritten by a fallback, and a found fallback is never overwritten by a later one.
Example ¶
ExampleWithFallbackPrefix looks up SMCORE_PORT first, then SMP_PORT.
package main
import (
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
env := envx.New(
envx.WithPrefix("SMCORE"),
envx.WithFallbackPrefix("SMP"),
envx.WithLookup(envx.MapLookup(map[string]string{
"SMP_PORT": "8080",
})),
)
port := envx.Bind(env, "PORT", 0)
fmt.Println(port.Key(), port.Value())
}
Output: SMP_PORT 8080
func WithLookup ¶
WithLookup sets the function used to read environment variables. Default: os.LookupEnv. A nil function is ignored. Override for testing or to read from a custom source.
func WithPrefix ¶
WithPrefix sets a prefix prepended to all variable names, joined with "_". A trailing underscore in prefix is trimmed and the prefix is upper-cased. Default: empty string (no prefix).
Example: WithPrefix("APP") makes Bind(env, "PORT", 0) read "APP_PORT".
type Var ¶
type Var[T any] struct { // contains filtered or unexported fields }
Var holds a typed value bound to an environment variable. Use Var.Value to read the resolved value and Var.Ptr to get a pointer (useful for binding the same field to a clix flag).
When created by BindTo or BindRequiredTo, Var.Ptr aliases the caller's target pointer so env, struct field, and clix flag share one memory location.
func Bind ¶
Bind reads an environment variable and converts it to type T. When the variable is not set, defaultVal is used. A value that fails to parse is reported by Env.Validate as ErrInvalid.
Supported types: string, bool, int, int32, int64, uint, float64, exact time.Duration (ParseDuration, unit required), time.Time and named types convertible to it (RFC3339), []string (comma-separated), defined types whose underlying kind is a supported builtin (named int64 parses as an integer, not a duration), and types whose pointer implements encoding.TextUnmarshaler.
Example ¶
ExampleBind shows typed reads with defaults using an injected lookup so the example is deterministic.
package main
import (
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
env := envx.New(
envx.WithPrefix("APP"),
envx.WithLookup(envx.MapLookup(map[string]string{
"APP_PORT": "9090",
})),
)
port := envx.Bind(env, "PORT", 8080)
host := envx.Bind(env, "HOST", "localhost") // not set → default
if err := env.Validate(); err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%s:%d\n", host.Value(), port.Value())
}
Output: localhost:9090
Example (List) ¶
ExampleBind_list parses a comma-separated value into a []string.
package main
import (
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
env := envx.New(envx.WithLookup(envx.MapLookup(map[string]string{
"ORIGINS": "a.com, b.com ,c.com",
})))
origins := envx.Bind(env, "ORIGINS", []string{"localhost"})
fmt.Println(origins.Value())
}
Output: [a.com b.com c.com]
Example (Time) ¶
ExampleBind_time parses an RFC3339 timestamp, matching clix flag semantics.
package main
import (
"fmt"
"time"
"github.com/aasyanov/urx/envx"
)
func main() {
env := envx.New(envx.WithLookup(envx.MapLookup(map[string]string{
"STARTED_AT": "2025-01-02T15:04:05Z",
})))
started := envx.Bind(env, "STARTED_AT", time.Time{})
fmt.Println(started.Value().Format(time.RFC3339))
}
Output: 2025-01-02T15:04:05Z
func BindRequired ¶
BindRequired reads a required environment variable. When the variable is not set, Env.Validate reports it as ErrMissing. The resolved value is the zero value of T until the variable is provided.
Example ¶
ExampleBindRequired demonstrates the missing-variable report.
package main
import (
"errors"
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
env := envx.New(envx.WithLookup(envx.MapLookup(map[string]string{})))
envx.BindRequired[string](env, "SECRET")
err := env.Validate()
fmt.Println("missing reported:", errors.Is(err, envx.ErrMissing))
}
Output: missing reported: true
func BindRequiredTo ¶
BindRequiredTo is the required counterpart of BindTo: it writes the resolved value into *target and marks the variable required, so Env.Validate reports ErrMissing when it is absent. The current value of *target is used as the fallback until the variable is provided. Var.Ptr aliases target, same as BindTo.
Panics if target is nil — a nil destination is a programming error.
func BindTo ¶
BindTo reads an environment variable and writes the resolved value into *target. When the variable is not set, *target keeps its current value (serving as the default). This is the preferred way to overlay env vars onto a config struct loaded by cfgx:
port := envx.BindTo(env, "PORT", &cfg.Port) clix.AddFlag(port.Ptr(), "port", "p", cfg.Port, "listen port")
Var.Ptr returns target, so clix and the struct field stay in sync.
Panics if target is nil — a nil destination is a programming error.
Example ¶
ExampleBindTo overlays environment variables onto a config struct — the envx layer of the cfgx → envx → clix pipeline. Var.Ptr aliases the struct field so clix can override the same memory location.
package main
import (
"fmt"
"github.com/aasyanov/urx/envx"
)
func main() {
type Config struct {
Port int
Host string
}
cfg := Config{Port: 8080, Host: "localhost"} // defaults (or from cfgx)
env := envx.New(envx.WithLookup(envx.MapLookup(map[string]string{
"PORT": "9090", // HOST not set → keeps "localhost"
})))
port := envx.BindTo(env, "PORT", &cfg.Port)
envx.BindTo(env, "HOST", &cfg.Host)
// port.Ptr() == &cfg.Port — safe for clix.AddFlag(port.Ptr(), ...)
_ = port.Ptr()
if err := env.Validate(); err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%s:%d\n", cfg.Host, cfg.Port)
}
Output: localhost:9090
func (*Var[T]) Key ¶
Key returns the full environment variable name that supplied the value (primary or fallback). When the variable was not set, Key is the primary candidate — the same name BindRequired reports as missing when no fallbacks are configured.
func (*Var[T]) Ptr ¶
func (v *Var[T]) Ptr() *T
Ptr returns a pointer to the resolved value. For BindTo and BindRequiredTo this is the same pointer passed to the bind call, so clix.AddFlag(v.Ptr(), ...) updates the overlaid struct field directly.
type WalkOption ¶ added in v1.5.2
type WalkOption func(*walkConfig)
WalkOption configures Walk. The default key source is the `env` struct tag (allowlist): fields without `env` are skipped.
func KeysFromEnvTag ¶ added in v1.5.2
func KeysFromEnvTag() WalkOption
KeysFromEnvTag makes Walk yield only fields tagged `env:"NAME"` or structs tagged `env:",inline"`. This is the default.
func KeysFromJSON ¶ added in v1.5.2
func KeysFromJSON() WalkOption
KeysFromJSON derives relative keys from `json` tags (UPPER, kebab to `_`, inline flatten). Empty tags fall back to the Go field name.
func KeysFromTOML ¶ added in v1.5.2
func KeysFromTOML() WalkOption
KeysFromTOML derives relative keys from `toml` tags (UPPER, kebab to `_`, inline flatten). Empty tags fall back to the Go field name.
func KeysFromYAML ¶ added in v1.5.2
func KeysFromYAML() WalkOption
KeysFromYAML derives relative keys from `yaml` tags the way yamlio does: tag name, kebab-case to `_`, UPPER, `,inline` flatten. Opt-in for YAML product configs; not the library default (cfgx also loads JSON/TOML).