Skip to content

Repository files navigation

envcfg

Parse environment variables into Go structs with minimal boilerplate and first-class support for complex data structures

Go Reference Go Report Card Coverage Test Status Release License

Key Features

✨ Zero Configuration Philosophy

  • Field names just work (snake or camel)
  • Nested structs automatically create prefixes
  • No special tags needed

🔄 Explicit Tag Matching

  • Reuse the json/yaml/toml tags a struct already has, without adding env tags
  • You declare which tags to match and in what order, so it stays deterministic

🎛️ Configuration Without Struct Tags

  • Declare required fields, defaults and names at the call site with WithField
  • Works on structs you don't own, where you can't add tags at all

🎯 Simple Environment Variables

  • One string value per variable
  • No need to encode complex data in environment values

🧩 Complex Types Made Simple

  • Maps: SETTINGS_KEY=value
  • Arrays: PORTS_0=8080
  • Nested structs: REDIS_HOST=localhost
  • Slice of structs: SERVERS_0_HOST=host1
  • Map of structs: DATABASES_PRIMARY_HOST=host1

📦 Compatible With Popular Libraries

  • Most features from popular libraries available

🛠️ Fully Configurable

  • No lock-in to specific tag formats, tags are all configurable
  • Default settings are all configurable
  • Parsers/decoders are all configurable

Table of Contents

Installation

go get github.com/sethpollack/envcfg

Quick Start

Set the following environment variables:

# string
export NAME=name
# integer
export PORT=8080
# float
export RATE=1.23
# boolean
export IS_ENABLED=true
# duration
export TIMEOUT=60s
# nested struct
export REDIS_HOST=localhost
export REDIS_PORT=6379
# delimited slice
export TAGS=tag1,tag2,tag3
# indexed slice
export PORTS_0=8080
export PORTS_1=9090
# slice of structs
export SERVERS_0_HOST=localhost1
export SERVERS_0_PORT=8080
export SERVERS_1_HOST=localhost2
export SERVERS_1_PORT=9090
# key-value map
export LABELS=key1:value1,key2:value2
# flat map
export SETTINGS_KEY1=1
export SETTINGS_KEY2=2
# map of structs
export DATABASES_PRIMARY_HOST=localhost1
export DATABASES_PRIMARY_PORT=6379
export DATABASES_SECONDARY_HOST=localhost2
export DATABASES_SECONDARY_PORT=6380

Parse the environment variables into a struct:

package main

import (
	"fmt"
	"time"

	"github.com/sethpollack/envcfg"
)

func main() {
	type ServerConfig struct {
		Host string
		Port int
	}

	type Config struct {
		Name      string
		Port      int
		Rate      float64
		IsEnabled bool
		Timeout   time.Duration

		Redis ServerConfig

		Tags    []string
		Ports   []int
		Servers []ServerConfig

		Labels    map[string]string
		Settings  map[string]int
		Databases map[string]ServerConfig

	}

	cfg := Config{}
	if err := envcfg.Parse(&cfg); err != nil {
		panic(err)
	}

	fmt.Printf(`Config:
  Name: %s
  Port: %d
  Rate: %.2f
  IsEnabled: %t
  Timeout: %s
  Redis:
    Host: %s
    Port: %d
  Tags: %v
  Ports: %v
  Servers: [
    {Host: %s, Port: %d},
    {Host: %s, Port: %d}
  ]
  Labels: %v
  Settings: %v
  Databases: {
    primary: {Host: %s, Port: %d},
    secondary: {Host: %s, Port: %d}
  }
`,
		cfg.Name, cfg.Port, cfg.Rate, cfg.IsEnabled, cfg.Timeout,
		cfg.Redis.Host, cfg.Redis.Port,
		cfg.Tags, cfg.Ports,
		cfg.Servers[0].Host, cfg.Servers[0].Port,
		cfg.Servers[1].Host, cfg.Servers[1].Port,
		cfg.Labels, cfg.Settings,
		cfg.Databases["primary"].Host, cfg.Databases["primary"].Port,
		cfg.Databases["secondary"].Host, cfg.Databases["secondary"].Port,
	)
}

Results:

Config:
  Name: name
  Port: 8080
  Rate: 1.23
  IsEnabled: true
  Timeout: 1m0s
  Redis:
    Host: localhost
    Port: 6379
  Tags: [tag1 tag2 tag3]
  Ports: [8080 9090]
  Servers: [
    {Host: localhost1, Port: 8080},
    {Host: localhost2, Port: 9090}
  ]
  Labels: map[key1:value1 key2:value2]
  Settings: map[key1:1 key2:2]
  Databases: {
    primary: {Host: localhost1, Port: 6379},
    secondary: {Host: localhost2, Port: 6380}
  }

Tip

The example above demonstrates the three supported syntaxes for both slices and maps:

  • Slices:
    • delimited TAGS=tag1,tag2,tag3
    • indexed PORTS_0=8080
    • struct SERVERS_0_HOST=localhost
  • Maps:
    • key-value pairs LABELS=key1:value1
    • flat keys SETTINGS_KEY1=1
    • struct DATABASES_PRIMARY_HOST=localhost

Types

  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • bool
  • float32, float64
  • time.Duration
  • structs
  • slices
  • maps

Note

Type support can be extended using the WithKindParser and WithTypeParser options.

Collections

Indexes are discovered by scanning the environment, so a gap leaves a zero value rather than truncating or renumbering the rest:

export SERVERS_0_HOST=a
export SERVERS_2_HOST=c
# -> []Server{{Host: "a"}, {}, {Host: "c"}}

A slice is sized to its largest index, so the type you pick decides whether the gaps are materialized. Use map[int]T when you want only the indexes you set:

export SERVERS_0_HOST=a
export SERVERS_5_HOST=f
Servers []Server       // [{a} {} {} {} {} {f}]  length 6
Servers map[int]Server // map[0:{a} 5:{f}]       length 2

Setting both the delimited and the indexed form of the same field is an error, since there is no way to tell which one was meant:

export TAGS=a,b
export TAGS_0=z
# -> both delimited and indexed values found: Tags

Note

Map keys are lowercased, so DATABASES_PRIMARY_HOST becomes the key primary. Keys that need other casing aren't expressible.

Decoders

  • envcfg.Decoder
  • flag.Value
  • encoding.TextUnmarshaler
  • encoding.BinaryUnmarshaler

Note

Decoder support can be extended using the WithDecoder option.

Struct Tags

Every tag below has an equivalent call-site option, so tags are optional. See Field Overrides if you'd rather keep configuration out of your structs, or if the struct isn't yours to edit.

Name Description Default Example Tag Example Option
default Default value when environment variable is not set - default:"8080" env:",default=8080"
required Mark field as required false required:"true" env:",required"
notempty Ensure value is not empty false notempty:"true" env:",notempty"
expand Expand environment variables in value false expand:"true" env:",expand"
file Load value from file false file:"true" env:",file"
delim Delimiter for array values , delim:";" env:",delim=;"
sep Separator for map key-value pairs : sep:"=" env:",sep="
init Initialize nil pointers vars init:"always" env:",init=always"
ignore Ignore field false ignore:"true" env:",ignore" or env:"-"
decodeunset Decode unset environment variables false decodeunset:"true" env:",decodeunset"

Warning

When setting default values for slices, avoid using the comma as it conflicts with tag parsing. Either use a different delimiter or set array values using environment variables:

type Config struct {
    Tags []string `env:"TAGS,default=tag1,tag2"` // This will fail!
}

// Do this instead:
type Config struct {
    Tags []string `env:"TAGS,delim=;,default=tag1;tag2"` // Use a different delimiter
}

Init Options

  • vars - Initialize when values are present with the exception of structs that only have default values. (default)
  • any - Same as vars, but also initialize structs that only have default values.
  • always - Always initialize
  • never - Never initialize

Recursive Types

A self referential type only has a finite result when initialization is driven by values, since the data is what says where to stop:

type Node struct {
    Name string
    Next *Node
}
export ROOT_NAME=l0
export ROOT_NEXT_NAME=l1
export ROOT_NEXT_NEXT_NAME=l2
# -> three levels, then Next is nil

any and always do not depend on values, so on a recursive type they have no finite result and are reported rather than truncated:

cannot initialize recursive type: Root.Next.Next: init=always on Node,
set init=vars or init=never on this field

Opt the recursive field back to vars to keep the rest of your configuration on always. The mode carries down the cycle, so you only declare it once:

envcfg.Parse(&cfg,
    envcfg.WithInitAlways(),
    envcfg.WithField("Root", envcfg.WithField("Next", envcfg.InitVars())),
)

Tip

All defaults and tag names can be customized using the With* options. See Configuration Options for more details.

Field Name Mapping

By default envcfg matches on the env tag, then the struct field name, then the snake case field name, in that order. The first match wins.

os.Setenv("CUSTOM_URL",   "value") // Matches env tag
os.Setenv("DATABASEURL",  "value") // Matches struct field
os.Setenv("DATABASE_URL", "value") // Matches snake-case

type Config struct {
    DatabaseURL string `env:"custom_url"`
}

Use WithMatch to choose the matchers yourself. They are tried in the order you list them, so precedence is always explicit.

Matcher Matches on
Tag(name) The value of the named struct tag
Field() The struct field name
Snake() The snake case struct field name
AnyTag() Every remaining struct tag, in sorted tag name order

This is how you consume structs from other libraries without adding env tags to them, which matters most when the struct isn't yours to edit:

type Config struct {
    DatabaseURL string `json:"db_url"`
    DataSource  string `yaml:"data_source"`
}

envcfg.Parse(&cfg, envcfg.WithMatch(
    envcfg.Tag("json"),
    envcfg.Tag("yaml"),
    envcfg.Snake(),
))

AnyTag() matches whatever tags happen to be present, for when you don't know them up front. Tags that carry behavior rather than names (required, default, ...) are never matched on.

// try the env tag first, then anything else on the field
envcfg.Parse(&cfg, envcfg.WithMatch(envcfg.Tag("env"), envcfg.AnyTag()))

Tip

All environment variable matching is case insensitive.

Field Overrides

Anything a struct tag can say, WithField can say at the call site instead. This keeps the configuration in one readable place and works on structs you can't edit.

type Config struct {
    DatabaseURL string
    Port        int
    Timeout     time.Duration
}

envcfg.Parse(&cfg,
    envcfg.WithField("DatabaseURL", envcfg.Required(), envcfg.NotEmpty()),
    envcfg.WithField("Port", envcfg.Default("8080")),
    envcfg.WithField("Timeout", envcfg.Env("TIMEOUT_SECONDS")),
)

WithFields takes them as a map when you'd rather read the whole contract as a table:

envcfg.Parse(&cfg, envcfg.WithFields(map[string][]envcfg.FieldOption{
    "DatabaseURL": {envcfg.Required(), envcfg.NotEmpty()},
    "Port":        {envcfg.Default("8080")},
    "Timeout":     {envcfg.Env("TIMEOUT_SECONDS")},
}))
Option Description Equivalent tag
Env(names...) Variable name, relative to the enclosing scope; extra names are fallbacks env
RootEnv(name) Absolute variable name, ignoring the enclosing prefix -
Default(value) Value when the variable is not set default
Required() Fail when the variable is not found required
NotEmpty() Fail when the variable is set but empty notempty
Expand() Expand variables in the value expand
File() Read the value from the file the variable points to file
Delim(delim) Delimiter for array values delim
Sep(sep) Separator for map key-value pairs sep
Ignore() Skip the field ignore
DecodeUnset() Decode even when unset decodeunset
InitVars() / InitAny() / InitAlways() / InitNever() Nil pointer initialization init

A field path that doesn't exist is an error rather than a silent no-op:

no such field: "DatabseURL", did you mean "DatabaseURL"?

Nesting

WithField nests inside itself, so you can group a struct's fields without writing dotted paths. Names inside a nested block are relative to the enclosing field, which is what makes collections work without spelling out the index:

type Config struct {
    Servers []Server // SERVERS_0_HOSTNAME, SERVERS_1_HOSTNAME, ...
}

envcfg.Parse(&cfg,
    envcfg.WithField("Servers",
        envcfg.WithField("Host", envcfg.Env("HOSTNAME")),
    ),
)

Use RootEnv when a nested field should read a top-level variable instead of one under the accumulated prefix:

type Config struct {
    Server struct {
        Host        string // SERVER_HOST
        Environment string // ENVIRONMENT, not SERVER_ENVIRONMENT
    }
}

envcfg.Parse(&cfg,
    envcfg.WithField("Server",
        envcfg.WithField("Environment", envcfg.RootEnv("ENVIRONMENT")),
    ),
)

Functions

  • Parse - Parse environment variables into a struct pointer
  • MustParse - Same as Parse, but panics on error
  • ParseAs - Parse environment variables into a specific type
  • MustParseAs - Same as ParseAs, but panics on error

Important

envcfg only parses exported fields.

Configuration Options

Tag Overrides

Option Description Default
WithDelimiterTag Tag name for delimiter delim
WithSeparatorTag Tag name for separator sep
WithDecodeUnsetTag Tag name for decoding unset environment variables decodeunset
WithDefaultTag Tag name for default values default
WithExpandTag Tag name for expandable variables expand
WithFileTag Tag name for file variables file
WithNotEmptyTag Tag name for not empty variables notempty
WithRequiredTag Tag name for required variables required
WithIgnoreTag Tag name for ignored variables ignore
WithInitTag Tag name for initialization strategy init

Default Overrides

Option Description Default
WithDelimiter Sets the default delimiter for array and map values ,
WithSeparator Sets the default separator for map key-value pairs :
WithDecodeUnset Enables decoding unset environment variables by default false
WithInitVars Sets the initialization strategy to vars vars
WithInitAny Sets the initialization strategy to any vars
WithInitNever Sets the initialization strategy to never vars
WithInitAlways Sets the initialization strategy to always vars
WithExpand Enables environment variable expansion by default false
WithNotEmpty Enables validating that values are not empty by default false
WithRequired Enables marking fields as required by default false

Field Matching

Option Description Default
WithMatch Sets the matchers used to resolve field names, in order Tag("env"), Field(), Snake()

Field Overrides

Option Description
WithField Declares configuration for a single field, nestable
WithFields Declares configuration for several fields, keyed by path

Custom Parser Functions

Option Description
WithTypeParser Registers a custom type parser
WithTypeParsers Registers custom type parsers
WithKindParser Registers a custom kind parser
WithKindParsers Registers custom kind parsers

Custom Decoder Functions

Option Description
WithDecoder Registers a custom decoder function for a specific interface

Loaders

Option Description
WithLoader Registers a loader

Loader Options

Loader options configure how environment variables are loaded and filtered before being matched to struct fields. These options allow you to:

  • Add different sources of configuration (environment variables, files, external services)
  • Filter which environment variables are considered
  • Transform environment variable names before matching
  • Set default values for when environment variables are not found
Option Description
WithSource Adds a source to the loader
WithSources Adds multiple sources to the loader
WithOSEnvSource Adds OS environment variables as a source
WithMapEnvSource Uses the provided map of environment variables as a source
WithDotEnvSource Adds environment variables from a file as a source
WithPrefix Combines WithTrimPrefix and WithHasPrefix
WithSuffix Combines WithTrimSuffix and WithHasSuffix
WithTransform Adds a transform function that modifies environment variable keys
WithTrimPrefix Removes a prefix from environment variable names
WithTrimSuffix Removes a suffix from environment variable names
WithFilter Adds a custom filter to the loader
WithHasPrefix Adds a prefix filter to the loader
WithHasSuffix Adds a suffix filter to the loader
WithHasMatch Adds a regular expression filter to the loader
WithKeys Keeps only keys matching the given glob patterns (SERVERS_*, SERVERS_*_HOST, PORT_[0-9]); an invalid pattern is an error

Combining Filters

Filters inside a single loader are combined with OR, so adding more of them widens the set:

envcfg.WithLoader(
    envcfg.WithKeys("APP_*"),
    envcfg.WithKeys("*_PORT"),
)
// keeps APP_PORT, APP_HOST, OTHER_PORT

To narrow instead, nest loaders. The outer filters apply to what the inner loader produced, which gives you AND:

envcfg.WithLoader(
    envcfg.WithLoader(
        envcfg.WithKeys("APP_*"),
    ),
    envcfg.WithKeys("*_PORT"),
)
// keeps APP_PORT only

Note

WithHasMatch patterns are unanchored, so regexp.MustCompile("SERVERS_") also matches XX_SERVERS_0_HOST. Anchor with ^ and $ when you want an exact shape: regexp.MustCompile(^SERVERS_\d+_HOST$).

Configuration Sources

envcfg supports multiple configuration sources that can be used individually or combined. Built-in sources have dedicated With* functions, while custom sources and those maintained as separate Go modules (to keep dependencies isolated) can be added using WithSource.

Option Description
WithOSEnvSource Adds OS environment variables as a source
WithMapEnvSource Uses the provided map of environment variables as a source
WithDotEnvSource Adds environment variables from a .env file as a source
WithSource(awssm.New(...)) Adds AWS Secrets Manager as a source

Source Ordering

Sources are processed in the order they are added, with later sources taking precedence over earlier ones. This ordering allows you to:

  • Set default values by adding a source with defaults first
  • Override values by adding sources with higher precedence later
  • Create a hierarchy of configuration (e.g., defaults → shared config → environment-specific config → local overrides)
  envcfg.Parse(&cfg,
    envcfg.WithLoader(
      envcfg.WithMapEnvSource(map[string]string{
          "APP_PORT": "8080",
        "LOG_LEVEL": "info",
      }),
      envcfg.WithDotEnvSource(".env"),
      envcfg.WithSource(awssm.New(
        awssm.WithRegion("us-west-2"),
        awssm.WithSecretID("myapp/config"),
      )),
      envcfg.WithOSEnvSource(),
    ),
)

In this setup:

  1. Default values are loaded first
  2. Then values from the .env file are loaded next
  3. Then values from AWS Secrets Manager
  4. Finally, OS environment variables take precedence over both defaults and .env file

About

Parse environment variables into Go structs with minimal boilerplate and first-class support for complex data structures

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages