Go Struct Tags and Environment Variables

Struct tags in Go provide metadata to fields and are used heavily by the encoders in the stdlib’s encoding package. Here’s a typical use case for a struct tag:

import "encoding/json"

type ServerConfig struct {
    Port   int    `json:"port"`
    APIKey string `json:"apiKey"`
}

func main() {
    bytes, _ := json.Marshal(ServerConfig{
        Port:   1234,
        APIKey: "something secret",
    })

    fmt.Println(string(bytes))
}

The struct tags in this example give Go’s JSON encoder an explicit name to use when marshalling/unmarshalling ServerConfig structs. This is simple and declarative, you’re providing this information in-line with the struct’s field itself, so there’s only one source of truth. Here’s the output of the above example:

{"port":1234,"apiKey":"something secret"}
Background

My team recently deployed a 12-factor application into Production and as per factor III of the methodology, we’re storing our config in “the environment” (environment variables).

In vanilla Go, this means writing one of the following:

config := os.Getenv("ENV_KEY")
if config == "" {
    // fail if required
}

// parse and use environment variable

or

if config, ok := ok.Lookupenv("ENV_KEY"); !ok {
    // fail if required
} else {
    // parse and use environment variable
}

Of the two, my preference is the latter, because A an empty string might be valid configuration for a given struct string field, B the “ok idiom” is used elsewhere by the stdlib (think map access), so looks and feels natural/familiar and C the resulting code is a more syntactically obvious alternative to checking for an empty string.

My issue with this approach is that even when the logic is refactored out into a helper function, the responsibility of finding the value, parsing it, checking for errors and making decisions as to whether the configuration is required or not is that of the application (and the developer). Not to mention the code distance between declaring the fields on the struct and setting them from environment variables elsewhere.

This all feels very imperative, so I decided to write a library to take the pain out of factor III.

Enter env

Env is a very lightweight library that allows you to express your environmental configuration declaratively. It takes care of the boring stuff, which keeps your code slick and easy to reason with.

Simply pass env.Set a pointer to the struct you wish to populate and it’ll populate every field with an env tag from environment variables. For example, an env tag value of MYAPPLICATION_PORT for an integer field Port will find an environment variable called MYAPPLICATION_PORT and populate the Port field with the integer representation of the environment variable.

If your application can’t run without a field being set, you can specify a required tag to ensure that an error is returned if the environment variable is not found. Another error will be returned if the environment variable found cannot be parsed to the field’s type or if the field itself is not settable.

Field types

All of the fields you’ll likely want to set from configuration are supported by env and for each type, slice support is also provided:

  • string
  • bool
  • int (byte, int16, int32, int64)
  • uint (uint8, uint16, uint32, uint64)
  • time.Duration

Slice types

For slice fields, env will attempt to parse an environment variable as a comma-delimited collection of the data type you’ve specified; trimming as necessary.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/codingconcepts/env"
)

type awsConfig struct {
	Secret            string        `env:"SECRET" required:"true"`
	Region            string        `env:"REGION"`
	Port              int           `env:"PORT" required:"true"`
	Peers             []string      `env:"PEERS"`
	ConnectionTimeout time.Duration `env:"TIMEOUT"`
}

func main() {
	config := awsConfig{}
	if err := env.Set(&config); err != nil {
		log.Fatal(err)
	}

	fmt.Println(config)
}
$ ID=1 SECRET=shh PORT=1234 PEERS=localhost:1235,localhost:1236 TIMEOUT=5s go run main.go

Feel free to get in touch with issues/suggestions/pull requests!