Skip to content

A Go library for parsing struct tags from environment variables.

License

Notifications You must be signed in to change notification settings

sethvargo/go-envconfig

Repository files navigation

Envconfig

GoDoc

Envconfig populates struct field values based on environment variables or arbitrary lookup functions. It supports pre-setting mutations, which is useful for things like converting values to uppercase, trimming whitespace, or looking up secrets.

Usage

Define a struct with fields using the env tag:

type MyConfig struct {
  Port     string `env:"PORT"`
  Username string `env:"USERNAME"`
}

Set some environment variables:

export PORT=5555
export USERNAME=yoyo

Process it using envconfig:

package main

import (
  "context"
  "log"

  "github.com/sethvargo/go-envconfig"
)

func main() {
  ctx := context.Background()

  var c MyConfig
  if err := envconfig.Process(ctx, &c); err != nil {
    log.Fatal(err)
  }

  // c.Port = 5555
  // c.Username = "yoyo"
}

You can also use nested structs, just remember that any fields you want to process must be public:

type MyConfig struct {
  Database *DatabaseConfig
}

type DatabaseConfig struct {
  Port     string `env:"PORT"`
  Username string `env:"USERNAME"`
}

Configuration

Use the env struct tag to define configuration. See the godoc for usage examples.

  • required - marks a field as required. If a field is required, decoding will error if the environment variable is unset.

    type MyStruct struct {
      Port string `env:"PORT, required"`
    }
  • default - sets the default value for the environment variable is not set. The environment variable must not be set (e.g. unset PORT). If the environment variable is the empty string, envconfig considers that a "value" and the default will not be used.

    You can also set the default value to the value from another field or a value from a different environment variable.

    type MyStruct struct {
      Port string `env:"PORT, default=5555"`
      User string `env:"USER, default=$CURRENT_USER"`
    }
  • prefix - sets the prefix to use for looking up environment variable keys on child structs and fields. This is useful for shared configurations:

    type RedisConfig struct {
      Host string `env:"REDIS_HOST"`
      User string `env:"REDIS_USER"`
    }
    
    type ServerConfig struct {
      // CacheConfig will process values from $CACHE_REDIS_HOST and
      // $CACHE_REDIS_USER respectively.
      CacheConfig *RedisConfig `env:", prefix=CACHE_"`
    
      // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and
      // $RATE_LIMIT_REDIS_USER respectively.
      RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"`
    }
  • overwrite - force overwriting existing non-zero struct values if the environment variable was provided.

    type MyStruct struct {
      Port string `env:"PORT, overwrite"`
    }

    The rules for overwrite + default are:

    • If the struct field has the zero value and a default is set:

      • If no environment variable is specified, the struct field will be populated with the default value.

      • If an environment variable is specified, the struct field will be populate with the environment variable value.

    • If the struct field has a non-zero value and a default is set:

      • If no environment variable is specified, the struct field's existing value will be used (the default is ignored).

      • If an environment variable is specified, the struct field's existing value will be overwritten with the environment variable value.

  • delimiter - choose a custom character to denote individual slice and map entries. The default value is the comma (,).

    type MyStruct struct {
      MyVar []string `env:"MYVAR, delimiter=;"`
    export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"}
  • separator - choose a custom character to denote the separation between keys and values in map entries. The default value is the colon (:) Define a separator with separator:

    type MyStruct struct {
      MyVar map[string]string `env:"MYVAR, separator=|"`
    }
    export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"}
  • noinit - do not initialize struct fields unless environment variables were provided. The default behavior is to deeply initialize all fields to their default (zero) value.

    type MyStruct struct {
      MyVar *url.URL `env:"MYVAR, noinit"`
    }
  • decodeunset - force envconfig to run decoders even on unset environment variable values. The default behavior is to skip running decoders on unset environment variable values.

    type MyStruct struct {
      MyVar *url.URL `env:"MYVAR, decodeunset"`
    }

Decoding

Note

Complex types are only decoded or unmarshalled when the environment variable is defined or a default value is specified.

Durations

In the environment, time.Duration values are specified as a parsable Go duration:

type MyStruct struct {
  MyVar time.Duration `env:"MYVAR"`
}
export MYVAR="10m" # 10 * time.Minute

TextUnmarshaler / BinaryUnmarshaler

Types that implement TextUnmarshaler or BinaryUnmarshaler are processed as such.

json.Unmarshaler

Types that implement json.Unmarshaler are processed as such.

gob.Decoder

Types that implement gob.Decoder are processed as such.

Slices

Slices are specified as comma-separated values.

type MyStruct struct {
  MyVar []string `env:"MYVAR"`
}
export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"}

Note that byte slices are special cased and interpreted as strings from the environment.

Maps

Maps are specified as comma-separated key:value pairs:

type MyStruct struct {
  MyVar map[string]string `env:"MYVAR"`
}
export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"}

Structs

Envconfig walks the entire struct, including nested structs, so deeply-nested fields are also supported.

If a nested struct is a pointer type, it will automatically be instantianted to the non-nil value. To change this behavior, see Initialization.

Custom Decoders

You can also define your own decoders. See the godoc for more information.

Testing

Relying on the environment in tests can be troublesome because environment variables are global, which makes it difficult to parallelize the tests. Envconfig supports extracting data from anything that returns a value:

lookuper := envconfig.MapLookuper(map[string]string{
  "FOO": "bar",
  "ZIP": "zap",
})

var config Config
envconfig.ProcessWith(ctx, &envconfig.Config{
  Target:   &config,
  Lookuper: lookuper,
})

Now you can parallelize all your tests by providing a map for the lookup function. In fact, that's how the tests in this repo work, so check there for an example.

You can also combine multiple lookupers with MultiLookuper. See the GoDoc for more information and examples.