envconfig

package module
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Feb 13, 2026 License: MIT Imports: 9 Imported by: 0

README

envconfig

Go Reference Go Report Card Coverage

A small, dependency-free Go library for loading configuration from environment variables directly into your structs.

It supports nested structs, prefixes, defaults, required fields, slices, maps, arrays, pointers, durations, and custom (un)marshalers. A helper is provided to read variables from a .env file.

  • Zero dependencies
  • Simple, tag-driven API
  • Works with standard os.LookupEnv or a custom lookups
  • Optional .env file loader (supports comments, export, quoting, inline comments)

Installation

go get github.com/struct0x/envconfig

Quick start

package main

import (
	"fmt"

	"github.com/struct0x/envconfig"
)

type HTTPServer struct {
	Env     string            `env:"ENV"`
	Host    string            `env:"HOST" envDefault:"127.0.0.1"`
	Port    int               `env:"PORT" envRequired:"true"`
	Enabled bool              `env:"ENABLED"`
	Tags    []string          `env:"TAGS"`     // "a,b,c" -> []string{"a","b","c"}
	Headers map[string]string `env:"HEADERS"`  // "k1=v1,k2=v2"
	DB      *DBConfig         `envPrefix:"DB"` // nil when no DB_* vars are set
}

func main() {
	var cfg HTTPServer

	// Use OS environment by default
	if err := envconfig.Read(&cfg); err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
}

Example environment:

export PORT=8080
export ENABLED=true
export TAGS="alpha,beta"
export HEADERS="X-Req=abc,X-Trace=on"

Using a .env file

Use EnvFileLookup to source values from a .env file. Lines use KEY=VALUE, support comments and export statements, and handle quoted values with inline comments.

package main

import (
	"fmt"

	"github.com/struct0x/envconfig"
)

type App struct {
	Name string `env:"NAME" envDefault:"demo"`
	Port int    `env:"PORT" envRequired:"true"`
}

func main() {
	var cfg App

	if err := envconfig.Read(&cfg, envconfig.EnvFileLookup(".env")); err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
}

Notes:

  • If both the .env file and the OS define a key, the OS environment value wins.
  • EnvFileLookup falls back to OS env if a file cannot be read.

Tags

Add struct field tags to control how values are loaded:

  • env: the env variable name. Use env:"-" to skip a field.
  • envDefault: fallback value if the variable is not set.
  • envRequired:"true": marks the field as required, returns error when not set, and no default provided.
  • envPrefix: for struct-typed fields; prepends a prefix (with underscore) for all nested fields under that struct.

Precedence per field:

  1. Value from lookupEnv(name)
  2. envDefault (if present)
  3. Error if envRequired:"true"

Dynamic Environment Variables

For environment variables that can't be expressed via struct tags, like numbered sequences (USER_1, PASS_1, USER_2, PASS_2) – implement the EnvCollector interface:

package main

import (
	"github.com/struct0x/envconfig"
)

type Config struct {
	Credentials Credentials
}

type Credentials []Credential

type Credential struct {
	User string `env:"USER"`
	Pass string `env:"PASS"`
}

func (c *Credentials) CollectEnv(env envconfig.EnvGetter) error {
	// Read IDs from CREDS=0,1,2 
	var ids []string
	if err := env.ReadValue("CREDS", &ids); err != nil {
		return err
	}

	for _, id := range ids {
		var cred Credential
		// Reads CREDS_0_USER, CREDS_0_PASS, etc.                                                                                                                                                                                                                                                                                                                                                                              
		if err := env.ReadIntoStruct("CREDS_"+id, &cred); err != nil {
			return err
		}
		*c = append(*c, cred)
	}
	return nil
}

Tags are ignored when implementing EnvCollector. The EnvGetter provides three methods:

  • Lookup for raw access,
  • ReadValue for parsing single values, and
  • ReadIntoStruct for populating nested structs with full tag support.
Examples

Basic tags:

package main

type DB struct {
	Host string `env:"DB_HOST" envDefault:"localhost"`
	Port int    `env:"DB_PORT" envRequired:"true"`
}

Nested with prefix:

package main

type SubConfig struct {
	Enabled bool   `env:"ENABLED"`
	Mode    string `env:"MODE" envDefault:"safe"`
}

type Root struct {
	Name string     `env:"NAME"`
	Sub  *SubConfig `envPrefix:"SUB"` // Reads SUB_ENABLED, SUB_MODE
}

Skipping a field:

package main

type T struct {
	Ignored string `env:"-"`
}

Supported types

  • string, bool
  • []byte - (read as []byte("bytes"), not []byte{'b','y','t','e','s'})
  • Integers: int, int8, int16, int32, int64
  • Unsigned integers: uint, uint8, uint16, uint32, uint64
  • Floats: float32, float64
  • time.Duration via time.ParseDuration
  • Arrays and slices (comma-separated values): "a,b,c"
  • Maps (comma-separated key=value pairs): "k1=v1,k2=v2"
  • Pointers to supported types (allocated only when a value is set; nil otherwise)
  • Custom types implementing in the following priority:
    • json.Unmarshaler > BinaryUnmarshaler > TextUnmarshaler

If a value cannot be parsed into the target type, Read returns a descriptive error.

Custom lookup (For Secret Managers, Vaults, etc.)

By default, Read uses os.LookupEnv, for more advanced use cases like reading values from secret managers like AWS Secret Manager, HashiCorp Vault you can provide a custom lookup function:

package main

import (
  "context"
  "os"
  "log/slog"

  "github.com/struct0x/envconfig"
)

type SecretResolver struct {
  startingCtx context.Context
  sm          SecretManager
}

func (s *SecretResolver) Lookup(key string) (string, bool) {
  val, ok := os.LookupEnv(key)
  if s.isSecret(val) {
    val, err := s.sm.ResolveSecret(s.startingCtx, val)
    if err != nil {
      slog.Error("missing value", "err", err)
      return "", false
    }
    return val, true
  }

  // fallback to standard lookup
  return val, ok
}

type C struct {
  N int `env:"N"`
}

func main() {
  sm := &SecretResolver{ /*...*/ }
	
  var c C
  _ = envconfig.Read(&c, sm.Lookup)
}

Error handling is your responsibility, use envRequired to ensure values are present regardless of lookup failures.

Additional lookup functions

IgnoreEmptyEnvLookup treats empty env vars as unset.

Error handling

Read returns an error when:

  • env tag is empty
  • Struct with env tag but no unmarshal interface
  • Exported values without env tag
  • EnvCollector with value (non-pointer) receiver
  • A required field is missing and no default is provided
  • holder is nil or not a pointer to a struct
  • Struct fields specify both env and envPrefix
  • envPrefix is empty when present
  • Parsing/conversion failures (returned errors includes the env key)
  • Unsupported leaf types (that do not implement a supported unmarshal interface)

Errors include the env variable name and context to aid debugging.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EnvFileLookup

func EnvFileLookup(filePath string) func(string) (string, bool)

EnvFileLookup returns a lookup function that reads environment variables from a .env file. It falls back to OS env if a file is not found. The .env file should have lines in the format KEY=VALUE. Comments starting with # are ignored. Empty lines are ignored. Notes:

  • If both the .env file and OS environment define a key, the OS environment value wins.
  • Lines like `export KEY=VALUE` are supported.

func IgnoreEmptyEnvLookup

func IgnoreEmptyEnvLookup(key string) (string, bool)

IgnoreEmptyEnvLookup wraps os.LookupEnv but treats empty values as unset. If the variable is present but "", it returns ok == false.

func Read

func Read[T any](holder *T, lookupEnv ...LookupEnv) error

Read populates holder (a pointer to struct) using the provided lookup function to resolve values.

Usage:

type C struct {
  Port int    `env:"PORT" envDefault:"8080"`
  TLS  struct {
    Enabled bool   `env:"ENABLED"`
    Cert    string `env:"CERT" envRequired:"true"`
  } `envPrefix:"TLS"` // effective keys: TLS_ENABLED, TLS_CERT
}
var cfg C
if err := envconfig.Read(&cfg); err != nil { log.Fatal(err) }

Lookup source:

By default Read uses os.LookupEnv. You may pass a custom lookup function,
e.g., envconfig.Read(&cfg, myLookup) where myLookup has signature func(string) (string, bool).

Tags (per field):

  • `env:"NAME"` : the environment variable name for this field. Use `env:"-"` to skip the field entirely.
  • `envDefault:"VAL"` : fallback used only when the variable is UNSET (i.e., lookup returns ok == false). If the variable is present but empty ("", ok == true), the empty value is used and default does NOT apply.
  • `envRequired:"true"`: if the variable is UNSET and no envDefault is provided, Read returns an error. Only the literal "true" enables this behavior.
  • `envPrefix:"PFX"` : for struct-typed fields (including embedded/ anonymous ones). Applies a prefix to all descendant leaf env names. Prefixes are joined with "_". Example: `envPrefix:"DB"` -> DB_HOST, DB_PORT.

Embedded and named struct fields:

  • Embedded (anonymous) and named struct fields are treated "flat" by default (no extra prefix). To prefix a subtree, put `envPrefix` on the field.

Advanced use with EnvCollector:

EnvCollector is a special interface that can be implemented on a pointer receiver.
When encountered, it will be called, moving env reading into the implementor.
This is an advanced interface that helps to populate values that cannot be represented using struct tags.

Supported field types:

  • primitives: string, bool, all int/uint sizes, float32/64
  • time.Duration (parsed via time.ParseDuration)
  • arrays, slices: comma-separated values (e.g. "a,b,c")
  • maps: comma-separated k=v pairs (e.g. "k1=v1,k2=v2"); split on first "="
  • pointers to any supported type (allocated only when a value is set; left nil otherwise - including pointer-to-struct fields where no descendant env var is found)
  • any type implementing (in the priority) json.Unmarshaler > encoding.BinaryUnmarshaler > encoding.TextUnmarshaler

Precedence per leaf field:

  1. If lookupEnv returns (value, ok==true), that value is used as-is (even if value is the empty string "").
  2. Else, if `envDefault` is present, it is used.
  3. Else, if `envRequired:"true"`, Read returns an error.
  4. Else, the field is left at its zero value.

Errors when:

  • `env` tag is empty
  • Struct with `env` tag but no unmarshal interface
  • Exported values without `env` tag
  • EnvCollector with value (non-pointer) receiver
  • A required field is missing and no default is provided
  • holder is nil or not a pointer to a struct
  • Struct fields specify both `env` and `envPrefix`
  • `envPrefix` is empty when present
  • Parsing/conversion failures (returned errors includes the env key)
  • Unsupported leaf types (that do not implement a supported unmarshal interface)

Types

type EnvCollector added in v0.3.0

type EnvCollector interface {
	// CollectEnv is called with an EnvGetter for reading env values.
	CollectEnv(env EnvGetter) error
}

EnvCollector is an advanced interface for collecting custom environment variables that can't be easily expressed via struct tags. For example, a custom collector can handle environment variables with complex naming conventions like USER_1, PASS_1, USER_2, PASS_2.

See TestEnvCollector for a concrete example.

type EnvGetter added in v0.3.0

type EnvGetter interface {
	// Lookup performs a raw lookup for an environment variable.
	Lookup(key string) (string, bool)

	// ReadValue parses a single environment variable into a target.
	// Target must be a pointer.
	ReadValue(key string, target any) error

	// ReadIntoStruct populates the target struct adding envPrefix + "_" for all `env` tags found on a struct.
	// Target must be a pointer to a struct
	ReadIntoStruct(envPrefix string, target any) error
}

EnvGetter provides a convenient way to get values from env variables. It is passed to EnvCollector.CollectEnv to allow a custom env collection. Under the hood it uses the provided Lookup in Read function.

type LookupEnv added in v0.3.0

type LookupEnv = func(string) (string, bool)

Jump to

Keyboard shortcuts

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