env

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 7 Imported by: 0

README

go-env-parser

Explicit, batch-validated parsing of environment variables into typed Go values.

go get github.com/cblauvelt/go-env-parser

Why

Most env-parsing libraries either bind struct tags (implicit, hard to grep) or return an error per call (fail on the first problem). This library does neither:

  • Every variable is read with an explicit function call — the mapping between env var and field is visible at the call site.
  • A Parser accumulates errors as it goes. One Err() call at the end reports every missing or malformed variable in a single message.
  • Values are read through a Lookuper interface, so tests inject a MapLookuper directly instead of reaching for os.Setenv / t.Setenv.

Quick start

import env "github.com/cblauvelt/go-env-parser"

p := env.New()
cfg := Config{
    DBHost:  p.RequiredString("DB_HOST"),
    DBPort:  p.Int("DB_PORT", 5432),
    Debug:   p.Bool("DEBUG", false),
    Timeout: p.Duration("TIMEOUT", 30*time.Second),
}
if err := p.Err(); err != nil {
    log.Fatal(err) // all problems reported in one message
}

If DB_HOST is unset and DB_PORT contains "abc" (a typo), the error from p.Err() names both problems:

env "DB_HOST" is required but not set
env "DB_PORT": invalid int value "abc": ...

Required vs defaulted accessors

Every type has two families:

Family Behavior on absent/malformed
String, Int, Bool, … Returns def. Never records an error. Malformed present values fire the bad-default hook.
RequiredString, RequiredInt, RequiredBool, … Records a missing or parse error. Returns the zero value of T.

Supported types

Scalars
Method Type
String / RequiredString string
Int / RequiredInt int
Int32 / RequiredInt32 int32
Int64 / RequiredInt64 int64
Uint / RequiredUint uint
Uint64 / RequiredUint64 uint64
Float64 / RequiredFloat64 float64
Bool / RequiredBool bool (accepts 1/0, true/false, t/f, TRUE/FALSE, True/False)
Duration / RequiredDuration time.Duration (e.g. "300ms", "1.5h")
Time / RequiredTime time.Time (RFC 3339)
TimeLayout / RequiredTimeLayout time.Time (custom layout)
Positive numbers

Timeouts, size caps, and rate limits are almost always required to be greater than zero, and a zero or negative value is a configuration mistake rather than a meaningful setting. These accessors fold that rule into the parse, so a non-positive value is treated exactly like an unparseable one.

Method Type
PositiveInt / RequiredPositiveInt int
PositiveInt64 / RequiredPositiveInt64 int64
PositiveFloat64 / RequiredPositiveFloat64 float64
PositiveDuration / RequiredPositiveDuration time.Duration
// A zero, negative, or unparseable value falls back to the default and fires
// the bad-default hook.
timeout := p.PositiveDuration("AUTHORIZATION_TIMEOUT", 15*time.Minute)
maxBody := p.PositiveInt64("MAX_BODY_BYTES", 65536)

// The Required forms record an error instead.
rate := p.RequiredPositiveFloat64("RATE_LIMIT_PER_SOURCE")
Collections
// []string — comma-separated, each element trimmed, empty elements dropped
origins := p.CSV("CORS_ORIGINS", []string{"*"})

// alternate separator
tags := p.CSVSep("TAGS", ":", nil)

// required: error if the key is absent (an empty value is not an error)
hosts := p.RequiredCSV("ALLOWED_HOSTS")

// []int
ports := p.IntSlice("PORTS", []int{8080})
ports := p.RequiredIntSlice("PORTS")
Validators

OneOf — enum membership:

level := p.OneOf("LOG_LEVEL", "info", "debug", "info", "warn", "error")
level := p.RequiredOneOf("LOG_LEVEL", "debug", "info", "warn", "error")

Validated[T] — generic escape hatch for any parse+validate logic:

parseURL := func(s string) (*url.URL, error) {
    u, err := url.Parse(s)
    if err != nil || u.Scheme == "" {
        return nil, fmt.Errorf("must be an absolute URL")
    }
    return u, nil
}

u := env.RequiredValidated(p, "WEBHOOK_URL", parseURL)
u := env.Validated(p, "WEBHOOK_URL", defaultURL, parseURL)

Cross-variable constraints

Some rules span more than one variable. These three helpers check pure presence across a set of keys, append at most one error each into the same Err() result as every accessor, and respect WithEmptyAsUnset (a set-but-empty variable counts as unset). They never panic on zero or one key.

AllOrNone — the keys must be all set or all unset. Use it for values that only make sense together, such as a certificate and its private key:

p.AllOrNone("TLS_CERT", "TLS_KEY")

MutuallyExclusive — at most one of the keys may be set. Use it for competing sources of the same thing:

p.MutuallyExclusive("DB_DSN", "LOCAL_STORE")

RequiredWith — when the first key is set, every dependent must also be set. Use it for variables that a feature key turns from optional into required (one error per missing dependent):

p.RequiredWith("SESSION_KEY", "API_URL", "API_KEY")

Options

WithEmptyAsUnset

Treat KEY="" the same as an absent key. Off by default, so explicit-empty values are preserved.

p := env.New(env.WithEmptyAsUnset())
Bad-Default Hook

Called when a defaulted accessor finds a present-but-malformed value. Useful for logging typos that would otherwise be silently replaced by the default.

p := env.New(env.WithBadDefaultHook(func(key, raw string, err error) {
    slog.Warn("env var malformed, using default", "key", key, "raw", raw, "err", err)
}))

Testing

Inject a MapLookuper so tests never touch the real environment:

func TestLoadConfig(t *testing.T) {
    p := env.From(env.MapLookuper{
        "DB_HOST": "localhost",
        "DB_PORT": "5432",
        "DEBUG":   "true",
    })
    cfg, err := LoadConfig(p)
    // ...
}

A function that accepts a *env.Parser can be tested this way without os.Setenv, t.Setenv, or any global state.

Porting from explicit os.Getenv calls

Before:

var missing []string
dbURL := func(key string) string {
    v := os.Getenv(key)
    if v == "" { missing = append(missing, key) }
    return v
}("DB_URL")
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
if port == 0 { port = 5432 }
if len(missing) > 0 {
    return fmt.Errorf("missing: %s", strings.Join(missing, ", "))
}

After:

p := env.New()
dbURL := p.RequiredString("DB_URL")
port  := p.Int("DB_PORT", 5432)
if err := p.Err(); err != nil {
    return err
}

Documentation

Overview

Package env provides explicit, batch-validated parsing of environment variables into typed Go values.

Unlike struct-tag binding libraries, every variable is read with an explicit function call, keeping the mapping between env vars and fields easy to read and grep. A Parser accumulates errors as it goes so a single Err() call reports every missing or malformed variable in one pass, rather than failing on the first one.

Values are read through a Lookuper, which decouples parsing from the process environment. The default source is the OS environment; tests and layered configuration can inject a MapLookuper instead, avoiding global state such as os.Setenv / t.Setenv.

Index

Constants

View Source
const DefaultSeparator = ","

DefaultSeparator is the delimiter used by CSV and IntSlice when no explicit separator is supplied.

Variables

This section is empty.

Functions

func RequiredValidated

func RequiredValidated[T any](p *Parser, key string, parse func(string) (T, error)) T

RequiredValidated returns the value of key converted by parse. If key is unset or parse returns an error, it records an error and returns the zero value of T.

func Validated

func Validated[T any](p *Parser, key string, def T, parse func(string) (T, error)) T

Validated returns the value of key converted by parse, or def if key is unset or parse returns an error. A parse error on a present value fires the bad-default hook. It never records an error.

parse may both convert and validate: returning a non-nil error rejects the value. This is the generic escape hatch for types and rules the built-in accessors do not cover (e.g. *url.URL, net.IP, a bounded int).

It is a package-level function rather than a method because Go methods cannot declare their own type parameters.

Types

type Lookuper

type Lookuper interface {
	// Lookup returns the value for key and whether key was set. A set key with
	// an empty value returns ("", true).
	Lookup(key string) (value string, ok bool)
}

Lookuper resolves an environment variable key to its raw string value and reports whether the key was present at all. Distinguishing "present but empty" from "absent" is intentional: some callers treat KEY="" as a meaningful explicit value, others as unset (see WithEmptyAsUnset).

type MapLookuper

type MapLookuper map[string]string

MapLookuper reads from an in-memory map. A nil or missing entry is reported as unset. It is intended for tests and for composing layered configuration without touching the real environment.

func (MapLookuper) Lookup

func (m MapLookuper) Lookup(key string) (string, bool)

Lookup implements Lookuper against the backing map.

type OSLookuper

type OSLookuper struct{}

OSLookuper reads from the process environment via os.LookupEnv. It is the default source used by New.

func (OSLookuper) Lookup

func (OSLookuper) Lookup(key string) (string, bool)

Lookup implements Lookuper against the OS environment.

type Option

type Option func(*Parser)

Option configures a Parser.

func WithBadDefaultHook

func WithBadDefaultHook(fn func(key, raw string, err error)) Option

WithBadDefaultHook registers a callback invoked when a defaulted accessor finds a present value that fails to parse. The accessor still returns its default; the hook lets callers log the typo instead of silently swallowing it. fn is called with the key, the raw value, and the parse error.

func WithEmptyAsUnset

func WithEmptyAsUnset() Option

WithEmptyAsUnset makes a set-but-empty variable (KEY="") be treated as if the variable were absent: required accessors report it missing, and defaulted accessors return their default. This restores the semantics of callers that consider an empty value meaningless.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser reads environment variables through a Lookuper and accumulates any errors encountered. Required accessors that fail append an error; defaulted accessors never append an error but report malformed values through the onBadDefault hook before falling back to the supplied default.

The zero value is not usable; construct a Parser with New or From.

A Parser is not safe for concurrent use. Typical usage builds a config struct from a single goroutine at startup and then checks Err once.

func From

func From(src Lookuper, opts ...Option) *Parser

From returns a Parser that reads from src. A nil src defaults to OSLookuper.

func New

func New(opts ...Option) *Parser

New returns a Parser that reads from the OS environment.

func (*Parser) AllOrNone added in v0.2.0

func (p *Parser) AllOrNone(keys ...string)

AllOrNone records one error unless the named keys are all set or all unset. It is the constraint for values that only make sense together, such as a TLS certificate and its private key. The error names every key and lists which of them are set. It respects WithEmptyAsUnset: a set-but-empty variable counts as unset. Zero or one key always satisfies the constraint and records nothing.

func (*Parser) Bool

func (p *Parser) Bool(key string, def bool) bool

Bool returns the value of key parsed as a bool, or def if key is unset or malformed. A malformed value fires the bad-default hook. Accepted values are those of strconv.ParseBool: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.

func (*Parser) CSV

func (p *Parser) CSV(key string, def []string) []string

CSV returns the value of key split on DefaultSeparator, with each element trimmed of surrounding whitespace and empty elements dropped. It returns def if key is unset. If key is set but yields no non-empty elements, an empty (non-nil) slice is returned rather than def — a set value always overrides the default. It never records an error.

func (*Parser) CSVSep

func (p *Parser) CSVSep(key, sep string, def []string) []string

CSVSep behaves like CSV but splits on the supplied separator.

func (*Parser) Duration

func (p *Parser) Duration(key string, def time.Duration) time.Duration

Duration returns the value of key parsed as a time.Duration (e.g. "300ms", "1.5h", "2h45m"), or def if key is unset or malformed. A malformed value fires the bad-default hook.

func (*Parser) Err

func (p *Parser) Err() error

Err returns the accumulated errors joined into a single error, or nil if no required accessor has failed. Call it once after reading all variables.

func (*Parser) Float64

func (p *Parser) Float64(key string, def float64) float64

Float64 returns the value of key parsed as a float64, or def if key is unset or malformed. A malformed value fires the bad-default hook.

func (*Parser) Int

func (p *Parser) Int(key string, def int) int

Int returns the value of key parsed as a base-10 int, or def if key is unset or malformed. A malformed value invokes the bad-default hook (if set) and is otherwise silently replaced by def.

func (*Parser) Int32

func (p *Parser) Int32(key string, def int32) int32

Int32 returns the value of key parsed as a base-10 int32, or def if key is unset or malformed (including overflow). A malformed value fires the bad-default hook.

func (*Parser) Int64

func (p *Parser) Int64(key string, def int64) int64

Int64 returns the value of key parsed as a base-10 int64, or def if key is unset or malformed. A malformed value fires the bad-default hook.

func (*Parser) IntSlice

func (p *Parser) IntSlice(key string, def []int) []int

IntSlice returns the value of key split on DefaultSeparator with each element parsed as a base-10 int. It returns def if key is unset. A malformed element fires the bad-default hook and causes def to be returned. It never records an error.

func (*Parser) MutuallyExclusive added in v0.2.0

func (p *Parser) MutuallyExclusive(keys ...string)

MutuallyExclusive records one error when more than one of the named keys is set. It is the constraint for values that name competing sources of the same thing, such as two alternative binding stores. The error names every key and lists which of them are set. It respects WithEmptyAsUnset: a set-but-empty variable counts as unset. Zero or one key never conflicts and records nothing.

func (*Parser) OneOf

func (p *Parser) OneOf(key, def string, allowed ...string) string

OneOf returns the value of key if it is one of allowed, otherwise def. It returns def when key is unset. When key is set to a value outside allowed, the bad-default hook fires and def is returned. It never records an error.

def is not required to be a member of allowed; callers are responsible for supplying a sensible default.

func (*Parser) PositiveDuration added in v0.3.0

func (p *Parser) PositiveDuration(key string, def time.Duration) time.Duration

PositiveDuration returns the value of key parsed as a time.Duration greater than zero, or def if key is unset, malformed, or not positive. A rejected present value fires the bad-default hook.

func (*Parser) PositiveFloat64 added in v0.3.0

func (p *Parser) PositiveFloat64(key string, def float64) float64

PositiveFloat64 returns the value of key parsed as a float64 greater than zero, or def if key is unset, malformed, or not positive. A rejected present value fires the bad-default hook.

func (*Parser) PositiveInt added in v0.3.0

func (p *Parser) PositiveInt(key string, def int) int

PositiveInt returns the value of key parsed as a base-10 int greater than zero, or def if key is unset, malformed, or not positive. A rejected present value fires the bad-default hook.

func (*Parser) PositiveInt64 added in v0.3.0

func (p *Parser) PositiveInt64(key string, def int64) int64

PositiveInt64 returns the value of key parsed as a base-10 int64 greater than zero, or def if key is unset, malformed, or not positive. A rejected present value fires the bad-default hook.

func (*Parser) RequiredBool

func (p *Parser) RequiredBool(key string) bool

RequiredBool returns the value of key parsed as a bool. If key is unset or malformed it records an error and returns false.

func (*Parser) RequiredCSV

func (p *Parser) RequiredCSV(key string) []string

RequiredCSV returns the value of key split like CSV. If key is unset it records a missing error and returns nil. A key that is set but yields no non-empty elements is not an error and returns an empty slice.

func (*Parser) RequiredCSVSep

func (p *Parser) RequiredCSVSep(key, sep string) []string

RequiredCSVSep behaves like RequiredCSV but splits on the supplied separator.

func (*Parser) RequiredDuration

func (p *Parser) RequiredDuration(key string) time.Duration

RequiredDuration returns the value of key parsed as a time.Duration. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredFloat64

func (p *Parser) RequiredFloat64(key string) float64

RequiredFloat64 returns the value of key parsed as a float64. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredInt

func (p *Parser) RequiredInt(key string) int

RequiredInt returns the value of key parsed as a base-10 int. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredInt32

func (p *Parser) RequiredInt32(key string) int32

RequiredInt32 returns the value of key parsed as a base-10 int32. If key is unset or malformed (including overflow) it records an error and returns 0.

func (*Parser) RequiredInt64

func (p *Parser) RequiredInt64(key string) int64

RequiredInt64 returns the value of key parsed as a base-10 int64. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredIntSlice

func (p *Parser) RequiredIntSlice(key string) []int

RequiredIntSlice returns the value of key split on DefaultSeparator with each element parsed as a base-10 int. If key is unset, or any element fails to parse, it records an error and returns nil.

func (*Parser) RequiredOneOf

func (p *Parser) RequiredOneOf(key string, allowed ...string) string

RequiredOneOf returns the value of key if it is one of allowed. If key is unset it records a missing error; if key is set to a value outside allowed it records a validation error. On failure it returns "".

func (*Parser) RequiredPositiveDuration added in v0.3.0

func (p *Parser) RequiredPositiveDuration(key string) time.Duration

RequiredPositiveDuration returns the value of key parsed as a time.Duration greater than zero. If key is unset, malformed, or not positive it records an error and returns 0.

func (*Parser) RequiredPositiveFloat64 added in v0.3.0

func (p *Parser) RequiredPositiveFloat64(key string) float64

RequiredPositiveFloat64 returns the value of key parsed as a float64 greater than zero. If key is unset, malformed, or not positive it records an error and returns 0.

func (*Parser) RequiredPositiveInt added in v0.3.0

func (p *Parser) RequiredPositiveInt(key string) int

RequiredPositiveInt returns the value of key parsed as a base-10 int greater than zero. If key is unset, malformed, or not positive it records an error and returns 0.

func (*Parser) RequiredPositiveInt64 added in v0.3.0

func (p *Parser) RequiredPositiveInt64(key string) int64

RequiredPositiveInt64 returns the value of key parsed as a base-10 int64 greater than zero. If key is unset, malformed, or not positive it records an error and returns 0.

func (*Parser) RequiredString

func (p *Parser) RequiredString(key string) string

RequiredString returns the value of key. If key is unset it records a missing error and returns "".

func (*Parser) RequiredTime

func (p *Parser) RequiredTime(key string) time.Time

RequiredTime returns the value of key parsed as a time.Time using RFC 3339. If key is unset or malformed it records an error and returns the zero time.

func (*Parser) RequiredTimeLayout

func (p *Parser) RequiredTimeLayout(key, layout string) time.Time

RequiredTimeLayout returns the value of key parsed as a time.Time using the supplied reference layout. If key is unset or malformed it records an error and returns the zero time.

func (*Parser) RequiredUint

func (p *Parser) RequiredUint(key string) uint

RequiredUint returns the value of key parsed as a base-10 unsigned int. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredUint64

func (p *Parser) RequiredUint64(key string) uint64

RequiredUint64 returns the value of key parsed as a base-10 uint64. If key is unset or malformed it records an error and returns 0.

func (*Parser) RequiredWith added in v0.2.0

func (p *Parser) RequiredWith(key string, dependents ...string)

RequiredWith records one error per unset dependent when key is set. It is the constraint for values that become required once a feature key turns the feature on. When key is unset the dependents are not checked and nothing is recorded. It respects WithEmptyAsUnset: a set-but-empty key or dependent counts as unset.

func (*Parser) String

func (p *Parser) String(key, def string) string

String returns the value of key, or def if key is unset (or empty when WithEmptyAsUnset is in effect). It never records an error.

func (*Parser) Time

func (p *Parser) Time(key string, def time.Time) time.Time

Time returns the value of key parsed as a time.Time using RFC 3339, or def if key is unset or malformed. A malformed value fires the bad-default hook. Use TimeLayout for a non-RFC3339 layout.

func (*Parser) TimeLayout

func (p *Parser) TimeLayout(key, layout string, def time.Time) time.Time

TimeLayout returns the value of key parsed as a time.Time using the supplied reference layout (see the time package), or def if key is unset or malformed.

func (*Parser) Uint

func (p *Parser) Uint(key string, def uint) uint

Uint returns the value of key parsed as a base-10 unsigned int, or def if key is unset or malformed (including negative or overflow). A malformed value fires the bad-default hook.

func (*Parser) Uint64

func (p *Parser) Uint64(key string, def uint64) uint64

Uint64 returns the value of key parsed as a base-10 uint64, or def if key is unset or malformed. A malformed value fires the bad-default hook.

Jump to

Keyboard shortcuts

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