Implements conf.Load to populate tagged structs from a chain of Sources (env, .env, YAML/JSON/TOML, custom). Supports default values, slice separators, nested structs, pointer fields, and a Validator hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.8 KiB
conf
Tiny, reflective config loader for Go. Define a struct, tag the fields, call
conf.Load. Values come from one or more Sources — env vars, .env files,
YAML/JSON/TOML, or anything you implement yourself.
Install
go get git.juancwu.dev/juancwu/conf
Requires Go 1.26+.
Usage
Tag your struct and call Load. The first source that returns a value wins;
if no source has it, the default: tag is used.
package main
import (
"log"
"strings"
"time"
"git.juancwu.dev/juancwu/conf"
"git.juancwu.dev/juancwu/errx"
)
type Config struct {
BindAddr string `env:"BIND_ADDR" default:":8080"`
DatabaseURL string `env:"DATABASE_URL"`
SessionCookieSecure bool `env:"SESSION_COOKIE_SECURE" default:"true"`
SessionIdleTTL time.Duration `env:"SESSION_IDLE_TTL" default:"24h"`
JWTSecret []byte `env:"JWT_SECRET"`
WorkerConcurrency int `env:"WORKER_CONCURRENCY" default:"4"`
Tags []string `env:"TAGS" sep:","`
}
func (c *Config) Validate() error {
const op = "config.Validate"
if strings.TrimSpace(c.DatabaseURL) == "" {
return errx.New(op, "DATABASE_URL is required")
}
if len(c.JWTSecret) == 0 {
return errx.New(op, "JWT_SECRET is required")
}
return nil
}
func main() {
var cfg Config
if err := conf.Load(&cfg, conf.EnvSource()); err != nil {
log.Fatal(err)
}
_ = cfg
}
Multiple sources
Sources are tried in order — earliest wins. Put highest-precedence first:
dotenv, _ := conf.DotEnvFile(".env")
yamlBase, _ := conf.YAMLFile("config.yaml")
err := conf.Load(&cfg,
conf.EnvSource(), // process env wins
dotenv, // .env overrides yaml
yamlBase, // base defaults
)
.env files
src, err := conf.DotEnvFile(".env")
Supports KEY=value, export KEY=value, # comments, single/double quotes,
and \n \t \r \\ \" escapes inside double quotes.
YAML / JSON / TOML
File sources are flattened into env-style keys: nested maps join with _ and
keys are uppercased.
# config.yaml
bind_addr: ":9000"
session:
idle_ttl: 24h
cookie:
secure: true
becomes BIND_ADDR, SESSION_IDLE_TTL, SESSION_COOKIE_SECURE — matching the
same env: tags on your struct. JSONFile / TOMLFile work the same way.
Each loader has a *Reader variant for io.Reader.
Custom sources
type Source interface {
Lookup(key string) (string, bool)
}
Implement that to pull from Vault, SSM, a database, or wherever.
Tags
| Tag | Purpose |
|---|---|
env:"KEY" |
Key looked up in each Source. Required to bind. |
env:"-" |
Skip the field. |
default:"v" |
Used when no Source returns the key. |
sep:"," |
Slice separator (default ,). |
Untagged struct fields are recursed into, so you can group related values:
type Config struct {
HTTP HTTP
}
type HTTP struct {
Addr string `env:"HTTP_ADDR" default:":8080"`
}
Supported field types
string, []byte, bool, all sized int/uint, float32/float64,
time.Duration, time.Time (RFC3339), slices of any scalar above, pointers
to any of the above (left nil if unset and no default), and nested structs.
Validation
If your destination type implements Validate() error, it's called after
fields are populated. Return an error to fail the load.
Errors
All errors flow through errx with op
codes like conf.Load, so errors.Is / errors.As work as usual.
License
MIT — see LICENSE.