env

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2022 License: MIT Imports: 10 Imported by: 0

README ¶

logo

A lightweight package for loading environment variables into structs

awesome ci docs report codecov

📌 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

Why not just resolve the name automatically, like toUpperSnakeCase(fieldName)? It feels too clever to me :)

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 either using the default struct tag (has a higher priority) or by initializing the struct fields directly.

cfg := struct {
    Host string `env:"HOST" default:"localhost"` // either use the `default` tag...
    Port int    `env:"PORT"`
}{
    Port: 8080, // ...or initialize the struct field directly.
}
if err := env.Load(&cfg); err != nil {
    // handle error
}

fmt.Println(cfg.Host) // localhost
fmt.Println(cfg.Port) // 8080

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 built-in 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

Multiple providers can be combined into a single one using MultiProvider. The order of the providers matters: if the same key exists in more than one provider, the value from the last one will be used.

os.Setenv("HOST", "localhost")

p := env.MultiProvider(
    env.OS,
    env.Map{"PORT": "8080"},
)

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

fmt.Println(cfg.Host) // localhost
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 practice 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
Strict mode

For cases where most environment variables are required, strict mode is available, in which all variables without the default tag are treated as required. To enable this mode, use the WithStrictMode option:

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

var cfg struct {
	Host string `env:"HOST"` // (required)
	Port int    `env:"PORT" default:"8080"`
}
if err := env.Load(&cfg, env.WithStrictMode()); err != nil {
	var notSetErr *env.NotSetError
	if errors.As(err, &notSetErr) {
		fmt.Println(notSetErr.Names) // [HOST]
	}
}
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

Documentation ¶

Overview ¶

Package env provides an API for loading environment variables into structs. See the Load function documentation for details.

Index ¶

Examples ¶

Constants ¶

This section is empty.

Variables ¶

View Source
var ErrEmptyTagName = errors.New("env: empty tag name is not allowed")

ErrEmptyTagName is returned when the `env` tag is found but the name of the environment variable is empty.

View Source
var ErrInvalidArgument = errors.New("env: argument must be a non-nil struct pointer")

ErrInvalidArgument is returned when the argument provided to Load/LoadFrom is invalid.

View Source
var ErrInvalidTagOption = errors.New("env: invalid tag option")

ErrInvalidTagOption is returned when the `env` tag contains an invalid option, e.g. `env:"VAR,invalid"`.

View Source
var ErrUnsupportedType = errors.New("env: unsupported type")

ErrUnsupportedType is returned when the provided struct contains a field of an unsupported type.

View Source
var Usage = func(w io.Writer, vars []Var) {
	tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
	defer tw.Flush()

	fmt.Fprintf(tw, "Usage:\n")
	for _, v := range vars {
		fmt.Fprintf(tw, "\t%s\t%s", v.Name, v.Type)
		if v.Required {
			fmt.Fprintf(tw, "\trequired")
		} else {
			if v.Type.Kind() == reflect.String && v.Default == "" {
				v.Default = "<empty>"
			}
			fmt.Fprintf(tw, "\tdefault %s", v.Default)
		}
		if v.Desc != "" {
			fmt.Fprintf(tw, "\t%s", v.Desc)
		}
		fmt.Fprintf(tw, "\n")
	}
}

Usage prints a usage message documenting all defined environment variables. It will be called by Load/LoadFrom if the WithUsageOnError option is provided and an error occurs while loading environment variables. It is exported as a variable, so it can be changed to a custom implementation.

Functions ¶

func Load ¶

func Load(dst any, opts ...Option) error

Load loads environment variables into the provided struct using the OS Provider as their source. To specify a custom Provider, use the LoadFrom function. dst must be a non-nil struct pointer, otherwise Load returns ErrInvalidArgument.

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. If the tag is found but the name of the environment variable is empty, the error will be ErrEmptyTagName.

The following types are supported as struct fields:

See the strconv package from the standard library for parsing rules. Implementing the encoding.TextUnmarshaler interface is enough to use any user-defined type. Default values can be specified either using the `default` struct tag (has a higher priority) or by initializing the struct fields directly. Nested structs of any depth level are supported, but only non-struct fields are considered as targets for parsing. If a field of an unsupported type is found, the error will be ErrUnsupportedType.

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: marks the environment variable as required
  • expand: expands the value of the environment variable using os.Expand

If environment variables are marked as required but not set, an error of type NotSetError will be returned. If the tag contains an invalid option, the error will be ErrInvalidTagOption.

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

See their documentation for details.

Example ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

Example (DefaultValue) ¶
package main

import (
	"fmt"

	"github.com/junk1tm/env"
)

func main() {
	cfg := struct {
		Host string `env:"HOST" default:"localhost"` // either use the `default` tag...
		Port int    `env:"PORT"`
	}{
		Port: 8080, // ...or initialize the struct field directly.
	}
	if err := env.Load(&cfg); err != nil {
		// handle error
	}

	fmt.Println(cfg.Host) // localhost
	fmt.Println(cfg.Port) // 8080
}
Output:

Example (Expand) ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

Example (NestedStruct) ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

Example (Required) ¶
package main

import (
	"errors"
	"fmt"

	"github.com/junk1tm/env"
)

func main() {
	// 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]
		}
	}
}
Output:

func LoadFrom ¶

func LoadFrom(p Provider, dst any, opts ...Option) error

LoadFrom loads environment variables into the provided struct using the specified Provider as their source. See Load documentation for more details.

Example ¶
package main

import (
	"fmt"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

Types ¶

type Map ¶

type Map map[string]string

Map is an in-memory Provider implementation useful in tests.

func (Map) LookupEnv ¶

func (m Map) LookupEnv(key string) (string, bool)

LookupEnv implements the Provider interface.

type NotSetError ¶

type NotSetError struct {
	// Names is a slice of the names of the missing required environment
	// variables.
	Names []string
}

NotSetError is returned when environment variables are marked as required but not set.

func (*NotSetError) Error ¶

func (e *NotSetError) Error() string

Error implements the error interface.

type Option ¶

type Option func(*loader)

Option allows to customize the behaviour of the Load/LoadFrom functions.

func WithPrefix ¶

func WithPrefix(prefix string) Option

WithPrefix configures Load/LoadFrom to automatically add the provided prefix to each environment variable. By default, no prefix is configured.

Example ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

func WithSliceSeparator ¶

func WithSliceSeparator(sep string) Option

WithSliceSeparator configures Load/LoadFrom to use the provided separator when parsing slice values. The default one is space.

Example ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	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
}
Output:

func WithStrictMode ¶ added in v0.5.0

func WithStrictMode() Option

WithStrictMode configures Load/LoadFrom to treat all environment variables without the `default` tag as required. By default, strict mode is disabled.

Example ¶
package main

import (
	"errors"
	"fmt"

	"github.com/junk1tm/env"
)

func main() {
	// os.Setenv("HOST", "localhost")

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

func WithUsageOnError ¶ added in v0.3.0

func WithUsageOnError(w io.Writer) Option

WithUsageOnError configures Load/LoadFrom to write an auto-generated usage message to the provided io.Writer, if an error occurs while loading environment variables. The message format can be changed by assigning the global Usage variable to a custom implementation.

Example ¶
package main

import (
	"os"
	"time"

	"github.com/junk1tm/env"
)

func main() {
	// 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

type Provider ¶

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)
}

Provider represents an entity that is able to provide environment variables.

OS is the main Provider that uses os.LookupEnv.

func MultiProvider ¶ added in v0.7.0

func MultiProvider(ps ...Provider) Provider

MultiProvider combines multiple providers into a single one, which will contain the union of their environment variables. The order of the providers matters: if the same key exists in more than one provider, the value from the last one will be used.

Example ¶
package main

import (
	"fmt"
	"os"

	"github.com/junk1tm/env"
)

func main() {
	os.Setenv("HOST", "localhost")

	p := env.MultiProvider(
		env.OS,
		env.Map{"PORT": "8080"},
	)

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

	fmt.Println(cfg.Host) // localhost
	fmt.Println(cfg.Port) // 8080
}
Output:

type ProviderFunc ¶

type ProviderFunc func(key string) (value string, ok bool)

ProviderFunc is an adapter that allows using functions as Provider.

func (ProviderFunc) LookupEnv ¶

func (f ProviderFunc) LookupEnv(key string) (string, bool)

LookupEnv implements the Provider interface.

type Var ¶ added in v0.3.0

type Var struct {
	Name     string       // Name is the full name of the variable, including prefix.
	Type     reflect.Type // Type is the variable's type.
	Desc     string       // Desc is an optional description parsed from the `desc` tag.
	Default  string       // Default is the default value of the variable. If the variable is marked as required, it will be empty.
	Required bool         // Required is true, if the variable is marked as required.
	Expand   bool         // Expand is true, if the variable is marked to be expanded with [os.Expand].
	// contains filtered or unexported fields
}

Var contains information about the environment variable parsed from a struct field. It is exported as a part of the Usage function signature.

Directories ¶

Path Synopsis
Package assert provides common assertions to use with the standard testing package.
Package assert provides common assertions to use with the standard testing package.
dotimport
Package dotimport provides type aliases for the parent [assert] package.
Package dotimport provides type aliases for the parent [assert] package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL