The envx package provides fluent API for retrieving and validating environment variables. It allows for easy fetching, default setting, type conversion, and conditions checking of environment variables.
- Retrieve environment variables with fallbacks.
- Set default values.
- Enforce required variables.
- Validate variables against a set of conditions including range validations.
- Convert environment variable values to common types (string, boolean, duration, int, uint, float types, time.Time, etc.)
- Load environment variables directly into struct fields using reflection and struct tags
- Support for nested structures with proper prefix handling
Basic Retrieval
import "github.com/velmie/x/envx"
func main() {
chain := envx.Get("MY_ENV_VAR").Default("defaultValue")
value, err := chain.String()
if err != nil {
// handle error
}
fmt.Println(value)
}
Using Coalesce to Get the First Non-Empty Variable
val, _ := envx.Coalesce("VAR_1", "VAR_2", "VAR_3").Default("defaultValue").String()
Validations
// Ensure the variable is set
chain := envx.Get("MY_VAR").Required()
// Ensure the variable matches a regular expression
chain := envx.Get("MY_VAR").MatchRegexp(regexp.MustCompile("^value-\\d+$"))
// Ensure the variable is one of a set of values
chain := envx.Get("MY_VAR").OneOf("value1", "value2", "value3")
// String length validations
chain := envx.Get("MY_VAR").MinLength(3)
chain := envx.Get("MY_VAR").MaxLength(10)
chain := envx.Get("MY_VAR").ExactLength(8)
// Universal validation for any type
chain := envx.Get("ANY_TYPE_VAR").Min(5) // Works for strings (length) and numbers (value)
chain := envx.Get("ANY_TYPE_VAR").Max(10) // Works for strings (length) and numbers (value)
chain := envx.Get("ANY_TYPE_VAR").Range(5, 10) // Works for all numeric types
// Numeric range validations (legacy, but still supported)
chain := envx.Get("MY_INT_VAR").MinInt(5)
chain := envx.Get("MY_INT_VAR").MaxInt(100)
chain := envx.Get("MY_INT_VAR").IntRange(5, 100)
// Unsigned integer range validations
chain := envx.Get("MY_UINT_VAR").MinUint(5)
chain := envx.Get("MY_UINT_VAR").MaxUint(100)
chain := envx.Get("MY_UINT_VAR").UintRange(5, 100)
// Float range validations
chain := envx.Get("MY_FLOAT_VAR").MinFloat(1.5)
chain := envx.Get("MY_FLOAT_VAR").MaxFloat(99.5)
chain := envx.Get("MY_FLOAT_VAR").FloatRange(1.5, 99.5)
// Chain multiple validations
chain := envx.Get("MY_VAR").Required().NotEmpty().MatchRegexp(regexp.MustCompile("^value-\\d+$")).MinLength(8)
// ....
value, err := chain.String()
// ....
Conversions
// Basic types
valueStr, err := envx.Get("MY_STRING_VAR").String()
valueBool, err := envx.Get("MY_BOOL_VAR").Boolean()
valueDuration, err := envx.Get("MY_DURATION_VAR").Duration()
// Integer types
valueInt, err := envx.Get("MY_INT_VAR").Int()
valueInt64, err := envx.Get("MY_INT64_VAR").Int64()
// Unsigned integer types
valueUint, err := envx.Get("MY_UINT_VAR").Uint()
valueUint8, err := envx.Get("MY_UINT8_VAR").Uint8()
valueUint16, err := envx.Get("MY_PORT_VAR").Uint16()
valueUint32, err := envx.Get("MY_UINT32_VAR").Uint32()
valueUint64, err := envx.Get("MY_UINT64_VAR").Uint64()
// Float types
valueFloat32, err := envx.Get("MY_FLOAT32_VAR").Float32()
valueFloat64, err := envx.Get("MY_FLOAT64_VAR").Float64()
// Time type
valueTime, err := envx.Get("MY_TIME_VAR").Time("2006-01-02T15:04:05Z07:00")
// URL
valueURL, err := envx.Get("MY_URL_VAR").URL()
There are often cases when checks for multiple variables are the same. In order to avoid duplicating code, the package provides functionality for creating prototypes.
p := envx.CreatePrototype().WithRunners(envx.Required, envx.NotEmpty).WithPrefix("MY_PREFIX_")
v1 := p.Get("VAR1").String()
v2 := p.Get("VAR2").String()
A common case is obtaining values in order to fill a structure. The following example demonstrates how to simplify the handling of such scenarios.
type DatabaseCredentials struct {
Host string
Port int
Name string
User string
Password string
}
func DatabaseCredentialsFromEnv() (*DatabaseCredentials, error) {
cfg := new(DatabaseCredentials)
p := envx.CreatePrototype().WithRunners(envx.Required, envx.NotEmpty)
err := envx.Supply(
envx.Set(&cfg.Host, p.Get("DB_HOST").ValidURL().String),
envx.Set(&cfg.Port, p.Get("DB_PORT").ValidPortNumber().Int),
envx.Set(&cfg.Name, p.Get("DB_NAME").String),
envx.Set(&cfg.User, p.Get("DB_USER").String),
envx.Set(&cfg.Password, p.Get("DB_PASS").String),
)
if err != nil {
return nil, err
}
return cfg, nil
}
This approach allows to "collapse" multiple calls and error checks into one compact structure that groups these calls and errors.
You can build more complex structures using nested structures, like this:
type Service struct {
LogLevel string
DatabaseCredentials *DatabaseCredentials
}
func ServiceFromEnv() (*Service, error) {
cfg := new(Service)
err := envx.Supply(
envx.Set(&cfg.LogLevel, envx.Prefixed("MY_APP_").Get("LOG_LEVEL").Default("info").OneOf("warn", "error", "info").String),
envx.Set(&cfg.DatabaseCredentials, DatabaseCredentialsFromEnv),
)
if err != nil {
return nil, err
}
return cfg, nil
}
Sometimes there is a need to retrieve values in the form of a list. If there's also a need to check each item of the list, use the 'Each' method, which presents the current value as a list of variables and allows applying checks to each item of the list.
For example:
addresses, err := envx.Get("MY_LISTEN_ADDRESSES").Each().ValidListenAddress().StringSlice()
if err != nil {
//...
}
// ...
By default, the delimiter is a comma ",", but it accepts any string as a delimiter.
addresses, err := envx.Get("MY_LISTEN_ADDRESSES").Each("|").ValidListenAddress().StringSlice()
if err != nil {
//...
}
// ...
The library supports various slice types:
// String slices
strSlice, err := envx.Get("MY_STR_LIST").StringSlice() // default delimiter: ","
strSlice, err := envx.Get("MY_STR_LIST").StringSlice("|") // custom delimiter
uniqueStrSlice, err := envx.Get("MY_UNIQUE_LIST").UniqueStringSlice()
// Number type slices
intSlice, err := envx.Get("MY_INT_LIST").IntSlice()
int64Slice, err := envx.Get("MY_INT64_LIST").Int64Slice()
uintSlice, err := envx.Get("MY_UINT_LIST").UintSlice()
uint8Slice, err := envx.Get("MY_UINT8_LIST").Uint8Slice()
uint16Slice, err := envx.Get("MY_UINT16_LIST").Uint16Slice()
uint32Slice, err := envx.Get("MY_UINT32_LIST").Uint32Slice()
uint64Slice, err := envx.Get("MY_UINT64_LIST").Uint64Slice()
float32Slice, err := envx.Get("MY_FLOAT32_LIST").Float32Slice()
float64Slice, err := envx.Get("MY_FLOAT64_LIST").Float64Slice()
// Other types
boolSlice, err := envx.Get("MY_BOOL_LIST").BooleanSlice()
durationSlice, err := envx.Get("MY_DURATION_LIST").DurationSlice()
urlSlice, err := envx.Get("MY_URL_LIST").URLSlice()
timeSlice, err := envx.Get("MY_TIME_LIST").TimeSlice("2006-01-02")
The envx package provides functionality to load environment variables directly into struct fields using reflection and struct tags. This approach simplifies the process of loading configuration from environment variables.
import "github.com/velmie/x/envx"
type Config struct {
Host string `env:"HOST;required"`
Port int `env:"PORT;default(8080)"`
LogLevel string `env:"LOG_LEVEL;default(info);oneOf(debug,info,warn,error)"`
Timeout time.Duration `env:"TIMEOUT;default(10s)"`
Debug bool `env:"DEBUG;default(false)"`
}
func main() {
var cfg Config
err := envx.Load(&cfg)
if err != nil {
// handle error
}
// Use the config
fmt.Printf("Server will start at %s:%d\n", cfg.Host, cfg.Port)
}
The struct tag format is:
`env:"ENV_VAR_NAME;directive1;directive2(param);directive3(param1,param2)"`
ENV_VAR_NAME
: The name of the environment variable to loaddirective1
,directive2
, etc.: Directives that specify validation rules or other behaviors- Directives can have parameters in parentheses:
directive(param)
ordirective(param1,param2)
- Multiple directives are separated by semicolons
- Multiple environment variable names can be specified with comma separation:
env:"VAR1,VAR2,VAR3"
When multiple environment variable names are specified, they are tried in the order listed, and the first one that is set will be used. This is similar to the Coalesce
function:
type Config struct {
// Will try DATABASE_URL, then DB_URL, then DB_CONNECTION_STRING in order
DatabaseURL string `env:"DATABASE_URL,DB_URL,DB_CONNECTION_STRING"`
// Combines multiple variables with validation
APIKey string `env:"API_KEY_PROD,API_KEY;required;minLen(10)"`
}
Special tag formats:
- When using quoted variable names, the prefix is not applied:
env:"'EXACT_VAR_NAME'"
- will look for exactlyEXACT_VAR_NAME
without any prefixes. - When using a leading comma, the field name is automatically prepended:
env:",FALLBACK_NAME"
- will first try the field name (converted to UPPER_SNAKE_CASE), thenFALLBACK_NAME
. - When using only directives:
env:";required;default(value)"
- the field name (converted to UPPER_SNAKE_CASE) will be used.
This allows for flexible fallback strategies and migration paths when renaming environment variables.
The struct loader supports the following field types:
- Basic types:
string
,bool
,int
,int8
,int16
,int32
,int64
,uint
,uint8
,uint16
,uint32
,uint64
,float32
,float64
,time.Duration
- Complex types:
time.Time
,*url.URL
- Collections:
[]string
,[]int
,[]int64
,[]uint
,[]uint8
,[]uint16
,[]uint32
,[]uint64
,[]float32
,[]float64
,[]bool
,[]time.Duration
- Maps:
map[string]string
- Nested structures: Both embedded and explicitly tagged
The envx package now supports nested structures with proper prefix handling:
type DatabaseConfig struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
}
type APIConfig struct {
Endpoint string `env:"ENDPOINT"`
Timeout time.Duration `env:"TIMEOUT"`
}
type Config struct {
// Tagged nested structs - tag is used as prefix
Database DatabaseConfig `env:"DB"` // Will look for DB_HOST, DB_PORT, etc.
API APIConfig `env:"API"` // Will look for API_ENDPOINT, API_TIMEOUT
// Non-tagged nested struct - fields accessed directly
Logger struct {
Level string `env:"LOGGER_LEVEL"`
Output string `env:"LOGGER_OUTPUT"`
} // Will look for LOGGER_LEVEL, LOGGER_OUTPUT directly
// Pointer to struct works too
Metrics *struct {
Path string `env:"METRICS_PATH"`
Interval time.Duration `env:"METRICS_INTERVAL"`
} `env:"METRICS"` // Will look for METRICS_PATH, METRICS_INTERVAL
}
You can also nest structures multiple levels deep:
type CredentialsConfig struct {
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
}
type AuthProviderConfig struct {
URL string `env:"URL"`
Timeout time.Duration `env:"TIMEOUT"`
Credentials CredentialsConfig `env:"CREDENTIALS"`
}
type SystemConfig struct {
Auth AuthProviderConfig `env:"AUTH"`
}
type Config struct {
System SystemConfig `env:"SYSTEM"`
}
// This will look for:
// - SYSTEM_AUTH_URL
// - SYSTEM_AUTH_TIMEOUT
// - SYSTEM_AUTH_CREDENTIALS_USERNAME
// - SYSTEM_AUTH_CREDENTIALS_PASSWORD
required
: Field is required and must be set in environmentnotEmpty
: Value must not be emptydefault(value)
: Default value if environment variable is not setexpand
: Expand environment variable references in the value (e.g.,${VAR_NAME}
)
validURL
: Value must be a valid URLvalidIP
: Value must be a valid IP addressvalidPort
: Value must be a valid port number (0-65535)validDomain
: Value must be a valid domain namevalidListenAddr
: Value must be a valid listen address (format:host:port
)min(n)
: Universal validator that checks:- String length for string types
- Minimum value for numeric types
max(n)
: Universal validator that checks:- String length for string types
- Maximum value for numeric types
range(min,max)
: Universal range validator that works with all numeric typesminLen(n)
: Value must have at least n charactersmaxLen(n)
: Value must have no more than n charactersexactLen(n)
: Value must have exactly n charactersregexp(pattern)
: Value must match the regular expression patternoneOf(value1,value2,...)
: Value must be one of the specified values
delimiter(char)
: Delimiter for slice elements (default is comma)layout(format)
: Time format layout for parsing time.Time fields
validateMethod(methodName)
: Call a method on the struct to validate the field valuerequiredIfMethod(methodName)
: Field is required if the specified method returns trueconvertMethod(methodName)
: Call a method on the struct to convert the string value from environment variable to the field type
The loader can be configured with various options:
err := envx.Load(&cfg,
envx.WithPrefix("APP_"),
envx.WithPrefixFallback(true),
envx.WithFallbackPrefix("DEFAULT_"),
envx.WithCustomValidator("email", emailValidator))
Available options:
WithPrefix(prefix)
: Add a prefix to all environment variable names- The prefix is only automatically applied to the first name in the list of names
- The prefix is not applied to names in single quotes:
env:"'EXACT_NAME'"
WithPrefixFallback(enable)
: If enabled, falls back to non-prefixed names when prefixed ones are not setWithFallbackPrefix(prefix)
: Adds a secondary prefix for fallback when the primary prefix doesn't match- This is applied to all fallback names (names after the first one) when
WithPrefixFallback
is enabled
- This is applied to all fallback names (names after the first one) when
WithTagParser(parser)
: Use a custom tag parserWithCustomValidator(name, validator)
: Add a custom validation directiveWithTypeHandler(type, handler)
: Register a handler for a specific typeWithKindHandler(kind, handler)
: Register a handler for a specific reflection kind
You can create custom validators for your specific needs:
// Using a custom validator directive
emailValidator := func(ctx *envx.FieldContext, _ envx.Directive) error {
value, err := ctx.Variable.String()
if err != nil {
return err
}
if !strings.Contains(value, "@") || !strings.Contains(value, ".") {
return fmt.Errorf("invalid email format: %s", value)
}
return nil
}
err := envx.Load(&cfg, envx.WithCustomValidator("email", emailValidator))
// Or using a struct method
type Config struct {
Password string `env:"PASSWORD;validateMethod(ValidatePassword)"`
// Using custom conversion method
CustomField MyType `env:"CUSTOM_ENV;convertMethod(ConvertToMyType)"`
}
func (c *Config) ValidatePassword(password string) error {
if len(password) < 10 {
return errors.New("password is too weak")
}
return nil
}
// Custom conversion method takes a string and returns the desired type plus an error
func (c *Config) ConvertToMyType(value string) (MyType, error) {
// Custom parsing logic here
return MyType{Value: value}, nil
}
If no env
tag is specified, the field name is automatically converted to UPPER_SNAKE_CASE. The conversion properly handles acronyms in the field names:
type Config struct {
// Regular camelCase to UPPER_SNAKE_CASE conversions:
DatabaseURL string // Uses DATABASE_URL environment variable
ServerPort int // Uses SERVER_PORT environment variable
// Properly handling acronyms:
IDOfIP string // Uses ID_OF_IP environment variable
UserIDType string // Uses USER_ID_TYPE environment variable
IPAddress string // Uses IP_ADDRESS environment variable
ComplexURLParser string // Uses COMPLEX_URL_PARSER environment variable
}
This allows for a more intuitive mapping between struct field names and environment variable names, even when working with complex naming conventions and acronyms.
When using the struct loader with prefixes, the following rules apply:
-
Primary name with prefix: The prefix is only applied to the first name in the comma-separated list in the tag. For example, with
WithPrefix("APP_")
and a tagenv:"VAR1,VAR2"
, the system will look forAPP_VAR1
, not forAPP_VAR2
. -
Quoted names: If a name is enclosed in single quotes, the prefix is never applied to it. This allows for exact environment variable names. For example, with
WithPrefix("APP_")
and a tagenv:"'EXACT_VAR'"
, the system will look for exactlyEXACT_VAR
, notAPP_EXACT_VAR
. -
Leading comma: If a tag starts with a comma, the field name is automatically prepended to the list of names to try. For example, with a field named
Port
and a tagenv:",FALLBACK_PORT"
, the system will first try the environment variablePORT
(converted from the field name), and thenFALLBACK_PORT
. -
No names, only directives: If a tag contains only directives (e.g.,
env:";required;default(8080)"
), the field name is used as the environment variable name. For example, with a field namedPort
and a tagenv:";required"
, the system will look for the environment variablePORT
. -
Fallback prefix: When
WithPrefixFallback(true)
andWithFallbackPrefix("FALLBACK_")
are used, the fallback prefix is applied to secondary names (after the first name) when they are not quoted. For example, withWithPrefix("APP_")
,WithPrefixFallback(true)
,WithFallbackPrefix("DEFAULT_")
and a tagenv:"VAR1,VAR2"
, the system will look forAPP_VAR1
, thenDEFAULT_VAR2
, and thenVAR2
.