- Published on
Extending env to support structs
- Authors
Introduction
env is a simple and zero-dependency Go library, written by Carlos Becker, to parse environment variables into structs. A good introduction post on what env is can be found here.
The Before
While there is nothing wrong with using individual environment variables, it's easy to get into a bit of a mess.
Imagine a system that has different user levels (user, poweruser, admin, root), each of which needs a set of defaults. Doing this using individual environment variables could look something like this (Note: It's a crude example and is for demonstration purposes only):
package config
import (
"github.com/caarlos0/env/v6"
)
type Config struct {
LOGGING_LEVEL string `env:"LOGGING_LEVEL" envDefault:"info"`
// Some other variables for databases / application set-up etc
UserDefaultUsername string `env:"USER_DEFAULT_USERNAME" envDefault:"user-change-me"`
UserDefaultPassword string `env:"USER_DEFAULT_PASSWORD" envDefault:"useruser"`
UserDefaultEmail string `env:"USER_DEFAULT_EMAIL" envDefault:"user@change.me"`
UserDefaultPhone string `env:"USER_DEFAULT_PHONE" envDefault:"12345 6789"`
UserDefaultAddress string `env:"USER_DEFAULT_ADDRESS" envDefault:"123 change me avenue, somewhere"`
PowerUserDefaultUsername string `env:"POWERUSER_DEFAULT_USERNAME" envDefault:"poweruser-change-me"`
PowerUserDefaultPassword string `env:"POWERUSER_DEFAULT_PASSWORD" envDefault:"powerpower"`
PowerUserDefaultEmail string `env:"POWERUSER_DEFAULT_EMAIL" envDefault:"poweruser@change.me"`
PowerUserDefaultPhone string `env:"POWERUSER_DEFAULT_PHONE" envDefault:"12345 6789"`
PowerUserDefaultAddress string `env:"POWERUSER_DEFAULT_ADDRESS" envDefault:"123 change me avenue, somewhere"`
AdminDefaultUsername string `env:"ADMIN_DEFAULT_USERNAME" envDefault:"admin-change-me"`
AdminDefaultPassword string `env:"ADMIN_DEFAULT_PASSWORD" envDefault:"adminadmin"`
AdminDefaultEmail string `env:"ADMIN_DEFAULT_EMAIL" envDefault:"admin@change.me"`
AdminDefaultPhone string `env:"ADMIN_DEFAULT_PHONE" envDefault:"12345 6789"`
AdminDefaultAddress string `env:"ADMIN_DEFAULT_ADDRESS" envDefault:"123 change me avenue, somewhere"`
RootDefaultUsername string `env:"ROOT_DEFAULT_USERNAME" envDefault:"root-change-me"`
RootDefaultPassword string `env:"ROOT_DEFAULT_PASSWORD" envDefault:"rootroot"`
RootDefaultEmail string `env:"ROOT_DEFAULT_EMAIL" envDefault:"root@change.me"`
RootDefaultPhone string `env:"ROOT_DEFAULT_PHONE" envDefault:"12345 6789"`
RootDefaultAddress string `env:"ROOT_DEFAULT_ADDRESS" envDefault:"123 change me avenue, somewhere"`
}
func New() (*Config, error) {
cfg := &Config{}
env.Parse(cfg)
return cfg, nil
}
Using a Custom Parser Func
env provides the ability to define custom parser funcs, which we're going to use to convert each user level (user, poweruser, admin, root) into one environment variable, populated by a JSON string.
First, let's define a new struct to use for each user level:
type User struct {
DefaultUsername string `json:"DEFAULT_USERNAME"`
DefaultPassword string `json:"DEFAULT_PASSWORD"`
DefaultEmail string `json:"DEFAULT_EMAIL"`
DefaultPhone string `json:"DEFAULT_PHONE"`
DefaultAddress string `json:"DEFAULT_ADDRESS"`
}
Next, lets change the Config
struct to use the new User
struct instead of individual string
's.
type Config struct {
LOGGING_LEVEL string `env:"LOGGING_LEVEL" envDefault:"info"`
// Some other variables for databases / application set-up etc
UserDefaults User `env:"USER_DEFAULTS"`
PowerUserDefaults User `env:"POWER_USER_DEFAULTS"`
AdminDefaults User `env:"ADMIN_DEFAULTS"`
RootDefaults User `env:"ROOT_DEFAULTS"`
}
Lastly, we implement a custom parser funcs by:
- Using
ParseWithFuncs
(instead ofParse
) - Using
reflect.TypeOf
to ensure we're only implementing the custom parser func on environment variables that match our struct - Implementing the customer parser func, which unmarshalls the JSON string into the
User
struct and returns it.
func New() (*Config, error) {
cfg := &Config{}
err := env.ParseWithFuncs(cfg, map[reflect.Type]env.ParserFunc{
reflect.TypeOf(User{}): func(v string) (interface{}, error) {
user := User{}
if err := json.Unmarshal([]byte(v), &user); err != nil {
return nil, err
}
return user, nil
},
})
if err != nil {
return cfg, err
}
return cfg, nil
}
Now, we can use the config
package in our main
func to test it out
func main() {
c, err := config.New()
if err != nil {
panic(err)
}
fmt.Println(c)
}
And setting defaults for each user level is as simple as passing a JSON string through:
$ USER_DEFAULTS='{"DEFAULT_USERNAME": "user-change-me", "DEFAULT_PASSWORD": "useruser"}' POWER_USER_DEFAULTS='{"DEFAULT_USERNAME": "poweruser-change-me", "DEFAULT_PASSWORD": "powerpower"}' ADMIN_DEFAULTS='{"DEFAULT_USERNAME": "admin-change-me", "DEFAULT_PASSWORD": "adminadmin"}' ROOT_DEFAULTS='{"DEFAULT_USERNAME": "root-change-me", "DEFAULT_PASSWORD": "rootroot"}' go run main.go
&{info {user-change-me useruser } {poweruser-change-me powerpower } {admin-change-me adminadmin } {root-change-me rootroot }}