envcfg is a Go package for loading config from environment variables into struct fields of arbitrary types. It's designed with a few guiding principles:
- If there's a bug in your config values, it's far better to see it right when your application starts than at 3 AM Sunday morning when your app finally gets around to using that value. So handlers and other business logic components shouldn't read or parse env vars themselves; they should access config that's already been read and parsed into concrete Go types for them.
- It's nice to have one centralized place where it's easy to see all the config that your app (or component) needs.
- Loading config should require as little boilerplate as possible.
envcfg is inspired by the struct tag pattern used by Go when unmarshaling JSON or scanning database rows. .
Imagine you had a struct that held your app's config values, that looked something like this:
type myAppConfig struct {
Foo string
Bar int
RefreshInterval time.Duration
}
To load those values from environment variables, you would add struct tags for env
and optionally
default
, and then call envcfg.Load
with a pointer to an instance of that config struct, like
this:
type myAppConfig struct {
Foo string `env:"FOO" default:"hey there"`
Bar int `env:"BAR"`
RefreshInterval time.Duration `env:"REFRESH_INTERVAL" default:"2h30m"`
}
var conf myAppConfig
err := envcfg.Load(&conf)
if err != nil {
panic(err.Error())
}
// now start up your app with your nicely-populated config...
In the example above, our config object has a string
, an int
, and a time.Duration
. It
requires that there be environment variables set for "BAR" and "REFRESH_RATE". If those aren't set,
then envcfg.Load
will return an error. The FOO environment variable may also be set, but the
default of "hey there" will be used if not.
As demonstrated in the above example, envcfg already knows how to parse strings into many of the types built in to Go and its standard library. Here's the complete list:
int
bool
string
float32
float64
int8
int16
int32
int64
uint
uint8
uint16
uint32
uint64
time.Duration
time.Time
*url.URL
net.IP
net.HardwareAddr
*mail.Address
[]*mail.Address
*template.Template
If your struct has a field of some other type, you can tell envcfg how to parse a string into it by
registering your own parser function. You can register parsers for struct types, pointers to struct
types, arrays, and type aliases like type MyInt int
. The example below demonstrates registering a
custom parser function that will populate a *sql.DB
field on a struct.
type myAppConfig struct {
DB *sql.DB `env:"DATABASE_URL"`
}
func main() {
// to load config we need to instantiate our config struct and pass its pointer to envcfg.Load
var conf myAppConfig
err := envcfg.Load(&conf)
if err != nil {
fmt.Println(err)
}
// now do something useful with the DB connection we just set up...
}
func LoadDBConnection(s string) (*sql.DB, error) {
db, err := sql.Open("postgres", s)
if err != nil {
return nil, err
}
return db, nil
}
func init() {
// A parser func takes one or more strings and returns the type matching your struct field,
// and an error.
if err := envcfg.RegisterParser(LoadDBConnection); err != nil {
panic(err.Error())
}
}
If you have a struct field that should be loaded from multiple environment variables, you can define
a parser function that takes several string arguments. The env
tag on your config struct field
must then provide the same number of environment variable names to be passed to the parser
(separated by commas). Here's an example that loads an Amazon S3 client from three commonly-used
environment variables:
package main
import (
"fmt"
"io/ioutil"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/nav-inc/envcfg"
)
type myAppConfig struct {
S3 *s3.S3 `env:"AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_DEFAULT_REGION"`
}
func main() {
var conf myAppConfig
err := envcfg.Load(&conf)
if err != nil {
panic(err.Error())
}
// now the "conf" object has an S3 client on it that you can use to get/post files.
}
// LoadS3Client takes an access key id, secret, and default region, and returns an Amazon S3 client
// instance.
func LoadS3Client(key, secret, region string) (*s3.S3, error) {
awsCreds := credentials.NewStaticCredentials(key, secret, "")
_, err := awsCreds.Get()
if err != nil {
return nil, fmt.Errorf("bad AWS credentials: %s", err.Error())
}
awsCfg := aws.NewConfig().WithCredentials(awsCreds).WithRegion(region).WithS3ForcePathStyle(true)
sess, err := session.NewSession()
if err != nil {
return nil, err
}
return s3.New(sess, awsCfg), nil
}
func init() {
if err := envcfg.RegisterParser(LoadS3Client); err != nil {
panic(err.Error())
}
}
If you want to provide your own map of values instead of reading environment variables, there's also
a envcfg.LoadFromMap function that will accept your own map[string]string
.
myVars := map[string]string{
"FOO": "Let's configure this",
"BAR": "1",
"DB": "postgres://postgres@/my_app?sslmode=disable",
}
...
err := envcfg.LoadFromMap(myVars, &conf)
The examples above all use the default loader provided by the envcfg package. If you want more control you can instantiate your own loader (or multiple loaders) using envcfg.New():
ec, err := envcfg.New()
if err != nil {
return err
}
err = ec.Load(&conf)
If you want a loader without any of the default parsers registered, you can get one by calling
envcfg.Empty()
:
ec := envcfg.Empty()
err := ec.RegisterParser(myParserFunc)
if err != nil {
return err
}
err = ec.Load(&conf)
The day after I wrote the first version of this library, a friend pointed out the similar envconfig library from Kelsey Hightower. The world is big enough for both. There are a few differences that may make you prefer one over the other.
This library (envcfg) lets you register parsers for other people's types without needing to alias
them to add UnmarshalText
or Decode
methods.
envcfg supports loading one field from multiple environment variables, while envconfig does not.
envconfig's support for Set(string) error
methods (like those in flag.Value
) should allow you to
have struct fields that can be set from env vars or command line flags. There is no such support in
envcfg.
If you don't designate the environment variable to use in a struct tag, the envconfig library
will use the field's capitalization to guess at the environment variable to use. envcfg, on the
other hand, will only attempt to load fields with explicit env
tags, and it requires that either
the environment variable or a default
tag (or both) be set.
envconfig can load any type that implements the TextUnmarshaler or BinaryUnmarshaler interfaces. Many stdlib types implement one of these. envcfg should do that too, but hasn't yet. This would allow it to delete a bunch of code that registers parser funcs for stdlib types.