A lightweight package for loading environment variables into structs

env

awesome-img ci-img docs-img report-img codecov-img license-img release-img

A lightweight package for loading environment variables into structs

📌 About

This package is made for apps that store config in environment variables. Its purpose is to replace multiple fragmented os.Getenv calls in main.go with a single struct definition, which simplifies config management and improves code readability.

📦 Install

go get github.com/junk1tm/env

🚀 Features

⚙️ Usage

Load is the main function of this package. It loads environment variables into the provided struct.

The struct fields must have the env:"VAR" struct tag, where VAR is the name of the corresponding environment variable. Unexported fields and fields without this tag (except nested structs) are ignored.

os.Setenv("PORT", "8080")

var cfg struct {
    Port int `env:"PORT"`
}
if err := env.Load(&cfg); err != nil {
    // handle error
}

fmt.Println(cfg.Port) // 8080

Supported types

The following types are supported as struct fields:

  • int (any kind)
  • float (any kind)
  • bool
  • string
  • time.Duration
  • encoding.TextUnmarshaler
  • slices of any type above

See the strconv package from the standard library for parsing rules.

Default values

Default values can be specified using basic struct initialization. They will be left untouched, if no corresponding environment variables are found.

os.Setenv("PORT", "8081")

cfg := struct {
    Port int `env:"PORT"`
}{
    Port: 8080, // default value, will be overridden by PORT.
}
if err := env.Load(&cfg); err != nil {
    // handle error
}

fmt.Println(cfg.Port) // 8081

Nested structs

Nested structs of any depth level are supported, but only non-struct fields are considered as targets for parsing.

os.Setenv("HTTP_PORT", "8080")

var cfg struct {
    HTTP struct {
        Port int `env:"HTTP_PORT"`
    }
}
if err := env.Load(&cfg); err != nil {
    // handle error
}

fmt.Println(cfg.HTTP.Port) // 8080

Customization

Provider

Load retrieves environment variables values directly from OS. To use a different source, try LoadFrom that accepts an implementation of the Provider interface as the first argument.

// Provider represents an entity that is able to provide environment variables.
type Provider interface {
    // LookupEnv retrieves the value of the environment variable named by the
    // key. If it is not found, the boolean will be false.
    LookupEnv(key string) (value string, ok bool)
}

Map is a builtin Provider implementation that might be useful in tests.

m := env.Map{"PORT": "8080"}

var cfg struct {
    Port int `env:"PORT"`
}
if err := env.LoadFrom(m, &cfg); err != nil {
    // handle error
}

fmt.Println(cfg.Port) // 8080

Tag-level options

The name of the environment variable can be followed by comma-separated options in the form of env:"VAR,option1,option2,...". The following tag-level options are supported:

Required

Use the required option to mark the environment variable as required. In case no such variable is found, an error of type NotSetError will be returned.

// os.Setenv("HOST", "localhost")
// os.Setenv("PORT", "8080")

var cfg struct {
    Host string `env:"HOST,required"`
    Port int    `env:"PORT,required"`
}
if err := env.Load(&cfg); err != nil {
    var notSetErr *env.NotSetError
    if errors.As(err, &notSetErr) {
        fmt.Println(notSetErr.Names) // [HOST PORT]
    }
}

Expand

Use the expand option to automatically expand the value of the environment variable using os.Expand.

os.Setenv("PORT", "8080")
os.Setenv("ADDR", "localhost:${PORT}")

var cfg struct {
	Addr string `env:"ADDR,expand"`
}
if err := env.Load(&cfg); err != nil {
	// handle error
}

fmt.Println(cfg.Addr) // localhost:8080

Function-level options

In addition to the tag-level options, Load also supports the following function-level options:

Prefix

It is a common practise to prefix app's environment variables with some string (e.g., its name). Such a prefix can be set using the WithPrefix option:

os.Setenv("APP_PORT", "8080")

var cfg struct {
    Port int `env:"PORT"`
}
if err := env.Load(&cfg, env.WithPrefix("APP_")); err != nil {
    // handle error
}

fmt.Println(cfg.Port) // 8080

Slice separator

Space is the default separator when parsing slice values. It can be changed using the WithSliceSeparator option:

os.Setenv("PORTS", "8080;8081;8082")

var cfg struct {
    Ports []int `env:"PORTS"`
}
if err := env.Load(&cfg, env.WithSliceSeparator(";")); err != nil {
    // handle error
}

fmt.Println(cfg.Ports[0]) // 8080
fmt.Println(cfg.Ports[1]) // 8081
fmt.Println(cfg.Ports[2]) // 8082

Usage on error

env supports printing an auto-generated usage message the same way the flag package does it. It will be printed if the WithUsageOnError option is provided and an error occurs while loading environment variables:

// os.Setenv("DB_HOST", "localhost")
// os.Setenv("DB_PORT", "5432")

cfg := struct {
    DB struct {
        Host string `env:"DB_HOST,required" desc:"database host"`
        Port int    `env:"DB_PORT,required" desc:"database port"`
    }
    HTTPPort int             `env:"HTTP_PORT" desc:"http server port"`
    Timeouts []time.Duration `env:"TIMEOUTS" desc:"timeout steps"`
}{
    HTTPPort: 8080,
    Timeouts: []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second},
}
if err := env.Load(&cfg, env.WithUsageOnError(os.Stdout)); err != nil {
    // handle error
}

// Output:
// Usage:
//   DB_HOST    string           required            database host
//   DB_PORT    int              required            database port
//   HTTP_PORT  int              default 8080        http server port
//   TIMEOUTS   []time.Duration  default [1s 2s 3s]  timeout steps

FAQ

Why force writing the name of the corresponding environment variable for each struct field instead of generating it from the field's name automatically?

It feels too magical. Remember:

Clear is better than clever

By writing the env tags just once, you free yourself (and the readers of your code) from having to mentally convert field names from CamelCame to UPPER_SNAKE_CASE each time you need to list the environment variables used in your project.

Bonus: Goland IDE users are able to configure live templates for struct tags under the Preferences | Editor | Live Templates settings. Simply duplicate the builtin json template, rename it to env and replace the FIELD_NAME variable expression with capitalizeAndUnderscore(fieldName()). Autocompletion for the env struct tag should work now!

What about loading from .env files for local development?

Currently, it's not the feature of this package, but there is a few options available. You can run export $(cat .env | xargs) before starting your program, use a directory-based autoloading shell extension (e.g. direnv) or combine env with the godotenv package:

import (
	_ "github.com/joho/godotenv/autoload" // loads .env file automatically
	"github.com/junk1tm/env"
)
Comments
  • loading from multiple providers

    loading from multiple providers

    Consider adding an option to load environment variables from multiple providers at once. It might be useful to merge variables from env.OS and env.File (if #8 is closed). It could be implemented as a wrapper around a slice of env.Provider, which implements the interface itself. The API would be something like this:

    // provider.go
    func MultiProvider(ps ...Provider) Provider { ... }
    
    // main.go
    p := env.MultiProvider(
    	env.File(), // first, load available variables from .env file
    	env.OS,     // then, if the same variables are provided by OS, override them
    )
    if err := env.LoadFrom(p, &cfg); err != nil {
    	// handle error
    }
    
  • feat: add WithVars options

    feat: add WithVars options

    This allows to create custom usage messages. Or something like this:

    package main
    
    import (
        "flag"
        "log"
        "os"
    
        "github.com/junk1tm/env"
    )
    
    func main() {
        cfg := struct {
            Host string `env:"HOST" default:"localhost" desc:"The host"`
            Port int    `env:"PORT" desc:"The port"`
        }{}
        vars := []env.Var{}
    
        err := env.LoadFrom(env.Map{}, &cfg, env.WithUsageOnError(os.Stderr), env.WithVars(&vars))
        if err != nil {
            log.Fatal(err)
        }
    
        flag.Usage = func() {
            env.Usage(os.Stderr, vars)
        }
    
        flag.Parse()
    }
    
  • `.env` file support

    `.env` file support

    Consider adding a native .env file support for local development. It could be implemented as another env.Provider:

    // provider.go
    func File(names ...string) Provider { ... } // names are optional. if nothing is provided, will look for .env
    
    // main.go
    if err := env.LoadFrom(env.File(), &cfg); err != nil {
    	// handle error
    }
    
  • default values from the `default` tag

    default values from the `default` tag

    Consider adding another option to provide default values - using the default struct tag. It might be useful for large configs with many environment variables: specifying the default value on the same line with the variable's name greatly improves readability. On the other hand, it completely ruins type safety (everything is a string), so the compiler won't be able to detect type-related errors. Also, having two different ways (struct initialization and the default tag) to do the same thing is probably a poor API design 🤔

    Anyway, here is an example:

    var cfg struct {
    	Port int `env:"PORT" default:"8080"`
    }
    if err := env.Load(&cfg); err != nil {
    	// handle error
    }
    
  • Replace sentinel errors with panic

    Replace sentinel errors with panic

    ErrInvalidArgument, ErrEmptyTagName, ErrUnsupportedType and ErrInvalidTagOption are flags of developer's mistakes, so in all these cases it makes sense to panic loudly instead of returning an error. As a bonus, this change would make the API smaller.

  • Consider making `StrictMode` a default behaviour

    Consider making `StrictMode` a default behaviour

    StrictMode configures Load/LoadFrom to treat all environment variables without the default tag as required.

    Most configuration libraries provide both required and default options, which leads to multiple possible combinations: only required, only default, no option at all (confusing) and required + default (a developer's mistake). With strict mode only the first two are possible, which makes the API simpler.

    In real-world applications it's common that most environment variables are required and only a few are optional. That's why this mode exists: some developers prefer to treat all variables as required by default and explicitly mark optional ones by specifying a default value.

    So why not just make this behaviour a default? If we did this, we wouldn't need the required tag and the WithStrictMode option at all, so the API would be smaller and, thus, easier to understand.

    Any opinions?

Un-marshaling environment variables to Go structs

envcfg Un-marshaling environment variables to Go structs Getting Started Let's set a bunch of environment variables and then run your go app #!/usr/bi

Sep 26, 2022
Lightweight package that makes easier and safer to deal with environment variables.

Envisage A lightweight package that makes easier and safer to deal with environment variables. Example Try it on On GoPlay https://goplay.tools/snippe

Apr 11, 2022
Read files into environment variables and execute command

read-file-to-env -- Read files into environment variables and execute command Example use: read-file-to-env -one-line=HOST=/etc/hostname sh -c 'echo h

Nov 12, 2021
Environment variables configuration package for Go microservices.

gocfg Environment variables configuration package for Go microservices. It helps validate environment variable values and set default values if needed

Dec 30, 2021
Small library to read your configuration from environment variables

envconfig envconfig is a library which allows you to parse your configuration from environment variables and fill an arbitrary struct. See the example

Nov 3, 2022
Go helpers to manage environment variables

Envh This library is made up of two parts : Env object : it wraps your environments variables in an object and provides convenient helpers. Env tree o

Sep 26, 2022
goconfig uses a struct as input and populates the fields of this struct with parameters from command line, environment variables and configuration file.

goconfig goconfig uses a struct as input and populates the fields of this struct with parameters from command line, environment variables and configur

Dec 15, 2022
A Go port of Ruby's dotenv library (Loads environment variables from `.env`.)

GoDotEnv A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) From the original Library: Storing configuration in the

Jan 5, 2023
🛠 A configuration library for Go that parses environment variables, JSON files, and reloads automatically on SIGHUP
🛠 A configuration library for Go that parses environment variables, JSON files, and reloads automatically on SIGHUP

config A small configuration library for Go that parses environment variables, JSON files, and reloads automatically on SIGHUP. Example func main() {

Dec 11, 2022
Golang library for managing configuration data from environment variables

envconfig import "github.com/kelseyhightower/envconfig" Documentation See godoc Usage Set some environment variables: export MYAPP_DEBUG=false export

Dec 26, 2022
A Go library for parsing struct tags from environment variables.

Envconfig Envconfig populates struct field values based on environment variables or arbitrary lookup functions. It supports pre-setting mutations, whi

Jan 2, 2023
Environment variables substitution for Go

envsubst Environment variables substitution for Go. see docs below Installation: From binaries Latest stable envsubst prebuilt binaries for 64-bit Lin

Jan 1, 2023
Quickly read variables from environment files

go-quick-env Quickly read variables from environment files The best way to import environment variables to your code, is by using .env files. This lib

May 11, 2021
A mapper of ENVironment variables to Structure for Go

envs a mapper of ENVironment variables to a Structure for Go. This library maps the environment variables to the struct according to the fields' types

Dec 3, 2021
Golang library for reading properties from configuration files in JSON and YAML format or from environment variables.

go-config Golang library for reading properties from configuration files in JSON and YAML format or from environment variables. Usage Create config in

Aug 22, 2022
formicidate is a small tool for Go application can update the value of environment variables in a .env file with code

formicidae Update .env files in Go with code. What is fomicidae? formicidate is a small tool for Go application. You can update the value of environme

Jan 23, 2022
An opinionated configuration loading framework for Containerized and Cloud-Native applications.
An opinionated configuration loading framework for Containerized and Cloud-Native applications.

Opinionated configuration loading framework for Containerized and 12-Factor compliant applications. Read configurations from Environment Variables, an

Dec 16, 2022
Tmpl - A tool to apply variables from cli, env, JSON/TOML/YAML files to templates

tmpl allows to apply variables from JSON/TOML/YAML files, environment variables or CLI arguments to template files using Golang text/template and functions from the Sprig project.

Nov 14, 2022
Library for setting values to structs' fields from env, flags, files or default tag

Configuration is a library for injecting values recursively into structs - a convenient way of setting up a configuration object. Available features:

Dec 7, 2022