~/blog/extending-env-to-support-structs
Published on

Extending env to support structs

965 words5 min read
Authors
  • avatar

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 of Parse)
  • 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   }}