Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

env

Go Reference Build Status

A library for declaring composable, reusable Go structs that load values parsed from environment variables.

✨ Features

🚀 Install

go get go.chrisrx.dev/x/env

Important

This package is in an experimental module, meaning the API might not (yet) be stable. When it graduates to its own module the path will change (e.g. go.chrisrx.dev/env) and will be aliased to the new module here, indefinitely.

📋 Usage

var opts = env.MustParseFor[struct {
	Addr           string        `env:"ADDR" default:":8080" validate:"split_addr().port > 1024"`
	Dir            http.Dir      `env:"DIR" $default:"tempdir()"`
	ReadTimeout    time.Duration `env:"READ_TIMEOUT" default:"2m"`
	WriteTimeout   time.Duration `env:"WRITE_TIMEOUT" default:"30s"`
	MaxHeaderBytes int           `env:"MAX_HEADER_BYTES" $default:"1 << 20"`
}](env.RootPrefix("FILESERVER"))

func main() {
	s := &http.Server{
		Addr:           opts.Addr,
		Handler:        http.FileServer(opts.Dir),
		ReadTimeout:    opts.ReadTimeout,
		WriteTimeout:   opts.WriteTimeout,
		MaxHeaderBytes: opts.MaxHeaderBytes,
	}
	log.Printf("serving %s at %s ...\n", opts.Dir, opts.Addr)
	if err := s.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

See testdata/pg for a more complete example of a reusable configuration struct.

Supported types

Basic types:

  • string, []byte
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool

Go slices, maps and structs are also supported.

Warning

Slices and maps cannot nest slices, maps or structs.

Any type that is convertible to a handled type can be used as well. For example, http.Dir is a type contructed from a string.

Built-in custom parsers:

  • time.Time, time.Duration
  • url.URL
  • rsa.PublicKey
  • x509.Certificate
  • net.HardwareAddr
  • net.IP

Any existing type that implements encoding.TextUnmarshaler will also work.

Note

Pointers to any types listed will also work.

Tags

The following struct tags are used to define how env reads from environment variables into values:

Name Description
env The name of the environment variable to load values from. If not present, the field is ignored.
default Specifies a default value for a field. This is used when the environment variable is not set.
$default Use an expression to set a default field value. This is used when the environment variable is not set.
validate Use a boolean expression to validate the field value.
required Set the field as required.
sep Separator used when parsing array/slice values. Defaults to ,.
layout Layout used to format/parse time.Time fields. Defaults to time.RFC3339Nano.

Auto-prefix

Important

Features like auto-prefix are important to making structs composable, which is why it is on by default.

Nested structs will automatically prepend the field name to the environment variable name for nested fields:

type Config struct {
    DB struct {
        Host string `env:"HOST"`
        Port int    `env:"PORT"`
    }
}

The above struct has a nested anonymous struct with the field name DB, which results in the nested fields values being loaded from DB_HOST and DB_PORT.

The prefix used for a struct can be set explicitly using the env tag on the struct itself:

type Config struct {
    DB struct {
        Host string `env:"HOST"`
        Port int    `env:"PORT"`
    } `env:"USERS_DB"`
}

Setting env:"USERS_DB" here means that the environment variables are now loaded from USERS_DB_HOST/USERS_DB_PORT.

Tip

You can prevent auto-prefix on a nested struct by declaring it as an anonymous field, aka embedding.

Registering custom parsers

The Register function can be used to define custom type parsers. It takes a non-pointer type parameter for the custom type and the parser function as the argument:

env.Register[net.IP](func(field Field, s string) (any, error) {
    return net.ParseIP(s), nil
})

The type parameter must be a non-pointer, but registering a type will always work with both the provided type and the pointer version without needing to register them both.

Default expressions

Default values can be generated using a Go-like expression language:

type Config struct {
    Start time.Time `env:"START" $default:"now()"`
}

Unlike default, the $default tag will always evaluate the tag value as an expression. There are quite a few builtins available that can be used and custom ones can be added. Functions like now() return a time.Time so that method chaining can be used to construct more complex expressions:

type Config struct {
    End time.Time `env:"END" $default:"now().add(duration('1h'))"`
}

A couple interesting things are happening here. For one, the above is syntactic sugar for time.Now().Add(-1 * time.Hour). It works by transforming the method lookups for Go types from snakecase to the expected Go method text case. For example, now().is_zero() will call time.Now().IsZero(), under-the-hood.

The other interesting thing happening here is that strings are specified using single quotes. This was a workaround to deal with the strict requirements for parsing struct tags which requires using double quotes to enclose tag values.

Field validation

The validate tag can be used to specify a boolean expression that checks the value of a field once parsing is finished. This can be used to verify things like minimum string length:

type Config struct {
    Name string `env:"NAME" validate:"len(Name) > 3"`
}

The field value is injected into the expression scope allowing for it to be referenced by the field name.

Tip

Along with the exact name, the pseudo-variable self can be used to refer to the field value