env is a configuration package for Go that bridges .env files, the process
environment and your Go structures.
It does three things, and does them well:
- Loads
.envfiles into the process environment — a small, explicit API (Load,Overload, …). - Maps the environment to and from Go structs — a familiar,
encoding/json-style API (Unmarshal,Marshal, …) with struct tags, defaults, validation and rich type support. - Parses
.envdata into plain maps without side effects (Read,Parse), so you can work with configuration from files,embed.FS, the network or a string.
The parser follows the de-facto .env format: single/double/backtick quotes,
escape sequences, multi-line values, inline comments, variable expansion and
the export prefix.
- Familiar API —
Load/Overloadfor files,Marshal/Unmarshalfor structs (the same shape asencoding/json). - Three destinations — load into the environment, a
map[string]string, or a typed struct; serialize a struct back to the environment, a map or a file. - Rich types — all integer and float sizes,
string,bool,url.URL,time.Duration,time.Time, nested structs, pointers, slices and arrays. - Struct tags — custom key names, default values, list separators, time
layouts, an
-to ignore a field and an inlinerequiredflag. - Spec-compliant parsing — quotes, escape sequences, multi-line values,
inline comments,
${VAR}/$VARexpansion andexport. - Side-effect-free variants —
Read/Parse/MarshalMap/UnmarshalMapnever touch the global environment, which keeps tests and multi-tenant code clean. - Typed errors —
errors.Is-friendly sentinels (ErrRequired,ErrNotPointer, …).
go get github.com/goloop/env/v2import "github.com/goloop/env/v2"Requires Go 1.24 or newer. The package has no third-party dependencies.
Given a .env file:
# Server configuration.
HOST=0.0.0.0
PORT=8080
ALLOWED_HOSTS=localhost:127.0.0.1
REQUEST_TIMEOUT=30sDescribe it with a struct and load it:
package main
import (
"fmt"
"log"
"time"
"github.com/goloop/env/v2"
)
type Config struct {
Host string `env:"HOST"`
Port int `env:"PORT" def:"80"`
AllowedHosts []string `env:"ALLOWED_HOSTS" sep:":"`
Timeout time.Duration `env:"REQUEST_TIMEOUT" def:"15s"`
}
func main() {
// Load the .env file into the process environment.
if err := env.Load(".env"); err != nil {
log.Fatal(err)
}
// Map the environment into the struct.
var cfg Config
if err := env.Unmarshal(&cfg); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", cfg)
// {Host:0.0.0.0 Port:8080 AllowedHosts:[localhost 127.0.0.1] Timeout:30s}
}Prefer to skip the global environment entirely (great for tests)? Read the file into a struct directly:
var cfg Config
if err := env.UnmarshalFile(".env", &cfg); err != nil {
log.Fatal(err)
}A typical service reads several namespaced subsystems from one file. Reuse a
single component struct with WithPrefix:
APP_HOST=0.0.0.0
APP_PORT=8080
DB_HOST=db.internal
DB_PORT=5432
DB_DSN=postgres://user:pass@db.internal:5432/app
REDIS_HOST=cache.internal
REDIS_PORT=6379type Endpoint struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
}
type Config struct {
App Endpoint `env:"APP"` // reads APP_HOST, APP_PORT
DB Endpoint `env:"DB"` // reads DB_HOST, DB_PORT
Redis Endpoint `env:"REDIS"` // reads REDIS_HOST, REDIS_PORT
DSN string `env:"DB_DSN,required"`
}
func main() {
if err := env.Load(".env"); err != nil {
log.Fatal(err)
}
var cfg Config
if err := env.Unmarshal(&cfg); err != nil {
log.Fatal(err) // e.g. "env: required key is not set: DB_DSN"
}
}Nested structs are namespaced automatically (the tag becomes a prefix joined
with _). The same Endpoint type is reused three times, and the required
flag turns a forgotten DB_DSN into a clear error instead of a silent zero
value.
Files / readers → process environment (variadic, defaults to .env):
| Function | Expands ${VAR} |
Overwrites existing keys |
|---|---|---|
Load(files...) |
yes | no |
Overload(files...) |
yes | yes |
LoadRaw(files...) |
no | no |
OverloadRaw(files...) |
no | yes |
LoadReader(r) |
yes | no |
MustLoad(files...) is like Load but panics on error (handy in init/main).
Files / readers → map (no side effects):
| Function | Expands ${VAR} |
|---|---|
Read(files...) (map, error) |
yes |
ReadRaw(files...) (map, error) |
no |
Parse(r) (map, error) |
yes |
ParseRaw(r) (map, error) |
no |
All(files...) iter.Seq2 |
yes (iterator) |
ReadSeq(files...) (iter.Seq2, error) |
yes (iterator + error) |
Struct mapping:
| Decode (→ struct) | Encode (struct →) |
|---|---|
Unmarshal(v, opts...) |
Marshal(v, opts...) → environment |
UnmarshalMap(m, v, opts...) |
MarshalMap(v, opts...) → map |
UnmarshalFile(name, v, opts...) |
MarshalFile(name, v, opts...) → file |
UnmarshalReader(r, v, opts...) |
MarshalWriter(w, v, opts...) → io.Writer |
UnmarshalString(s, v, opts...) |
MarshalString(v, opts...) → string |
Each UnmarshalFile/UnmarshalReader/UnmarshalString and MarshalFile/
MarshalWriter/MarshalString has a …Raw variant that skips ${VAR}/$VAR
expansion, so any value (including $, quotes and backticks) round-trips
verbatim.
Options: WithPrefix(p), WithSeparator(sep), WithTimeLayout(layout),
WithFileMode(mode) (for MarshalFile), WithParser[T]/WithEncoder[T] (custom
type handling), WithRequiredAll() (make every field required).
Environment helpers (thin wrappers over os): Get, Set, Unset,
Clear, Environ, Expand, Lookup, Exists.
Loadpopulates the real process environment (so child processes and libraries that reados.Getenvsee the values).UnmarshalFilefills a struct without touching the environment. They complement each other — pick the one that matches your goal.
type Config struct {
Host string `env:"HOST"` // key name
Port int `env:"PORT" def:"8080"` // default value
Hosts []string `env:"HOSTS" sep:","` // list separator
Started time.Time `env:"STARTED_AT" layout:"DateOnly"` // time layout
Token string `env:"TOKEN,required"` // must be present
Ignored string `env:"-"` // never mapped
}| Tag | Purpose | Default |
|---|---|---|
env |
key name; - ignores the field; ,required marks it mandatory |
field name |
def |
default value when the key is absent | zero value |
sep |
separator for slices/arrays | a comma |
layout |
layout for time.Time (Go layout or a constant name like DateOnly) |
RFC3339 |
string, bool, every sized int/uint, float32/float64, url.URL,
time.Duration (30s, 1h30m), time.Time, nested structs, pointers to any
of these, and slices/arrays of any of these.
# A full-line comment.
export HOST=localhost # the optional `export` prefix is allowed
PORT=8080 # an inline comment
EMPTY= # an empty value is valid -> ""
SPACED= trimmed # surrounding spaces of unquoted values are trimmed
DOUBLE="expands ${HOST}" # double quotes expand variables and escapes
SINGLE='literal ${HOST}' # single quotes are literal
BACKTICK=`literal ${HOST}` # backticks are literal
TABBED="a\tb" # \n \t \r \\ are interpreted in double quotes
MULTILINE="line one
line two" # quoted values may span several lines
LIST=a:b:c # split with a sep tag, e.g. sep:":"Variable expansion (${VAR} / $VAR) is resolved against earlier keys in the
file and the existing environment. Single quotes and backticks are literal.
See DOC.md for the full reference, every function, more examples and tips (Ukrainian: DOC.UK.md).
- Full reference and recipes: DOC.md · DOC.UK.md
- Package API: pkg.go.dev/github.com/goloop/env/v2
Bug reports and pull requests are welcome. Please run go test ./...,
go vet ./... and gofmt -l . before submitting.
env is released under the MIT License. See LICENSE.