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!