Awesome
<div align="center">🔍 Load environment variables into a config struct
</div>📌 About
This package is made for apps that store config in environment variables.
Its purpose is to replace fragmented os.Getenv
calls in main.go
with a single struct definition,
which simplifies config management and improves code readability.
🚀 Features
- Support for all common types and user-defined types
- Configurable source of environment variables
- Options: required, expand, slice separator, name separator
- Auto-generated usage message
📦 Install
Go 1.20+
go get go-simpler.org/env
📋 Usage
Load
is the main function of the package.
It loads environment variables into the given struct.
The struct fields must have the env:"VAR"
struct tag,
where VAR
is the name of the corresponding environment variable.
Unexported fields are ignored.
os.Setenv("PORT", "8080")
var cfg struct {
Port int `env:"PORT"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.Port) // 8080
Supported types
int
(any kind)float
(any kind)bool
string
time.Duration
encoding.TextUnmarshaler
- slices of any type above
- nested structs of any depth
See the strconv.Parse*
functions for the parsing rules.
User-defined types can be used by implementing the encoding.TextUnmarshaler
interface.
Nested structs
Nested struct of any depth level are supported, allowing grouping of related environment variables.
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
var cfg struct {
DB struct {
Host string `env:"DB_HOST"`
Port int `env:"DB_PORT"`
}
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.DB.Host) // localhost
fmt.Println(cfg.DB.Port) // 5432
If a nested struct has the optional env:"PREFIX"
tag,
the environment variables declared by its fields are prefixed with PREFIX
.
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB_"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.DB.Host) // localhost
fmt.Println(cfg.DB.Port) // 5432
Default values
Default values can be specified using the default:"VALUE"
struct tag.
os.Unsetenv("PORT")
var cfg struct {
Port int `env:"PORT" default:"8080"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.Port) // 8080
Required
Use the required
option to mark an environment variable as required.
If it is not set, an error of type NotSetError
is returned.
os.Unsetenv("PORT")
var cfg struct {
Port int `env:"PORT,required"`
}
if err := env.Load(&cfg, nil); err != nil {
var notSetErr *env.NotSetError
if errors.As(err, ¬SetErr) {
fmt.Println(notSetErr) // env: PORT is required but not set
}
}
Expand
Use the expand
option to automatically expand the value of an environment variable using os.Expand
.
os.Setenv("PORT", "8080")
os.Setenv("ADDR", "localhost:${PORT}")
var cfg struct {
Addr string `env:"ADDR,expand"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.Addr) // localhost:8080
Slice separator
Space is the default separator used to parse slice values.
It can be changed with Options.SliceSep
.
os.Setenv("PORTS", "8080,8081,8082")
var cfg struct {
Ports []int `env:"PORTS"`
}
if err := env.Load(&cfg, &env.Options{SliceSep: ","}); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.Ports) // [8080 8081 8082]
Name separator
By default, environment variable names are concatenated from nested struct tags as is.
If Options.NameSep
is not empty, it is used as the separator.
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB"`
}
if err := env.Load(&cfg, &env.Options{NameSep: "_"}); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.DB.Host) // localhost
fmt.Println(cfg.DB.Port) // 5432
Source
By default, Load
retrieves environment variables directly from OS.
To use a different source, pass an implementation of the Source
interface via Options.Source
.
type Source interface {
LookupEnv(key string) (value string, ok bool)
}
Here's an example of using Map
, a Source
implementation useful in tests.
m := env.Map{"PORT": "8080"}
var cfg struct {
Port int `env:"PORT"`
}
if err := env.Load(&cfg, &env.Options{Source: m}); err != nil {
fmt.Println(err)
}
fmt.Println(cfg.Port) // 8080
Usage message
The Usage
function prints a usage message documenting all defined environment variables.
An optional usage string can be added to environment variables with the usage:"STRING"
struct tag.
os.Unsetenv("DB_HOST")
os.Unsetenv("DB_PORT")
var cfg struct {
DB struct {
Host string `env:"DB_HOST,required" usage:"database host"`
Port int `env:"DB_PORT,required" usage:"database port"`
}
HTTPPort int `env:"HTTP_PORT" default:"8080" usage:"http server port"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
fmt.Println("Usage:")
env.Usage(&cfg, os.Stdout, nil)
}
Usage:
DB_HOST string required database host
DB_PORT int required database port
HTTP_PORT int default 8080 http server port
The format of the message can be customized by implementing the Usage([]env.Var, io.Writer, *env.Options)
method.
type Config struct{ ... }
func (Config) Usage(vars []env.Var, w io.Writer, opts *env.Options) {
for v := range vars {
// write to w.
}
}