Skip to content

proposal: encoding/env: populate a struct from environment variables #64891

@imjasonh

Description

@imjasonh

Proposal Details

Today, programs can query environment variables using methods in os: os.Getenv, os.Environ, os.LookupEnv, etc.

foo := os.Getenv("FOO")
bar, found := os.LookupEnv("BAR")
all := os.Environ()

This works and is very simple, but can get more complicated if you need to further validate or especially convert the string values to other types.

e := os.Getenv("NUM_FOOS")
if e == "" {
    // NUM_FOOS must be set!
}
i, err := strconv.Atoi(e)
if err != nil {
    // NUM_FOOS must be parseable as an int!
}

If a program relies on many environment variables to configure its behavior, a common smells can creep in: authors call os.Getenv from deep within their code, which can make it hard to test (T.Setenv helps)

The alternative, slightly beter, is to populate a struct at the top of their program from env vars, and pass around this struct after validation/conversion is done.

https://github.com/kelseyhightower/envconfig is a very popular, very simple module to make this second approach simpler, by populating a struct from env vars, with type conversion and basic validation built in and configurable using struct tags.

Before:

e := os.Getenv("NUM_FOOS")
if e == "" {
    // NUM_FOOS must be set!
}
i, err := strconv.Atoi(e)
if err != nil {
    // NUM_FOOS must be parseable as an int!
}
... for each env var to process

After:

var env struct {
    Debug       bool
    Port        int    `required:"true"`
    User        string `default:"foobar"`
    Users       []string
    Rate        float32
    Timeout     time.Duration
    ColorCodes  map[string]int
}
if err := envconfig.Process("", &env); err != nil {
    // Something went wrong!
}

The second arg lets callers pass a prefix to env vars, so using envconfig.Process("MY", &env) would populat TimeoutfromMY_TIMEOUT`.

envconfig currently has 12k+ dependents on GitHub: https://github.com/kelseyhightower/envconfig/network/dependents

It has no dependencies outside of stdlib: https://github.com/kelseyhightower/envconfig/blob/10e87fe9eaec671f89425dc366f004a9336bcc8f/go.mod

I propose that some functionality like this be included in the stdlib directly.


There may be functionality included in envconfig that Go authors don't consider worth including in stdlib, or would implement or expose differently, and IMO that's totally fine.

I'm mainly opening this to get feedback and gauge interest in such a thing being included by default.

Personally I'd propose dropping the MY_ prefixing feature, and rename the package to os/env (or just env? Or include it as os.ProcessEnv?). If the required and default struct tags make it, they should be namespaced like env:"required" etc.

If multiple values failed validation, all the errors should be returned with errors.Join.

There's also support for custom decoders -- perhaps those should leverage TextUnmarshaler?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions