gonfiguration 🔧: The Ultimate Golang Config Chaos

If you’re tired of writing the same env var parsing bullshit over and over, gonfiguration handles all the annoying shit for you. It’s got reflection-based mapping, supports all the types you actually use, and doesn’t crash when someone fucks up the config.

Here’s How It Works

package main
import (
	"fmt"
	"log"
	"os"
	"time"
	"github.com/psyb0t/gonfiguration"
)
type AppConfig struct {
	// Basic configuration
	AppName     string `env:"APP_NAME"`
	Debug       bool   `env:"DEBUG"`
	Port        int    `env:"PORT"`
	// String slices for lists
	AllowedHosts []string `env:"ALLOWED_HOSTS"`
	Features     []string `env:"FEATURES"`
	// Time duration fields
	Timeout      time.Duration `env:"TIMEOUT"`
	RetryDelay   time.Duration `env:"RETRY_DELAY"`
	// Numeric types
	MaxUsers     int64   `env:"MAX_USERS"`
	CacheSize    uint32  `env:"CACHE_SIZE"`
	LoadFactor   float64 `env:"LOAD_FACTOR"`
	// Database configuration
	DBDSN        string `env:"DB_DSN"`
	DBName       string `env:"DB_NAME"`
	DBUser       string `env:"DB_USER"`
	DBPass       string `env:"DB_PASS"`
}
func main() {
	cfg := AppConfig{}
	// Set sensible defaults
	gonfiguration.SetDefaults(map[string]interface{}{
		"APP_NAME":      "MyAwesomeApp",
		"DEBUG":         false,
		"PORT":          8080,
		"ALLOWED_HOSTS": []string{"localhost", "127.0.0.1", "*.example.com"},
		"FEATURES":      []string{"auth", "logging", "metrics"},
		"TIMEOUT":       30 * time.Second,
		"RETRY_DELAY":   5 * time.Second,
		"MAX_USERS":     1000,
		"CACHE_SIZE":    256,
		"LOAD_FACTOR":   0.75,
		"DB_DSN":        "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable",
	})
	// Set some env vars (normally these come from your environment)
	os.Setenv("APP_NAME", "ProductionBeast")
	os.Setenv("DEBUG", "true")
	os.Setenv("ALLOWED_HOSTS", "api.myapp.com, cdn.myapp.com , admin.myapp.com")
	os.Setenv("FEATURES", "auth,logging,metrics,analytics,caching")
	os.Setenv("TIMEOUT", "1m30s")
	os.Setenv("MAX_USERS", "50000")
	os.Setenv("DB_NAME", "production_db")
	os.Setenv("DB_USER", "app_user")
	os.Setenv("DB_PASS", "super-secret-password")
	// Parse it
	if err := gonfiguration.Parse(&cfg); err != nil {
		log.Fatalf("Config parsing fucked up: %v", err)
	}
	// Check what we got
	fmt.Printf("🚀 App: %s (Debug: %v)n", cfg.AppName, cfg.Debug)
	fmt.Printf("🌐 Listening on port: %dn", cfg.Port)
	fmt.Printf("🏠 Allowed hosts: %vn", cfg.AllowedHosts)
	fmt.Printf("⭐ Features enabled: %vn", cfg.Features)
	fmt.Printf("⏱️ Timeout: %v, Retry delay: %vn", cfg.Timeout, cfg.RetryDelay)
	fmt.Printf("👥 Max users: %d, Cache size: %d MBn", cfg.MaxUsers, cfg.CacheSize)
	fmt.Printf("📊 Load factor: %.2fn", cfg.LoadFactor)
	fmt.Printf("🗄️ Database: %s@%sn", cfg.DBUser, cfg.DBName)
}

Output:

🚀 App: ProductionBeast (Debug: true)
🌐 Listening on port: 8080
🏠 Allowed hosts: [api.myapp.com cdn.myapp.com admin.myapp.com]
⭐ Features enabled: [auth logging metrics analytics caching]
⏱️ Timeout: 1m30s, Retry delay: 5s
👥 Max users: 50000, Cache size: 256 MB
📊 Load factor: 0.75
🗄️ Database: app_user@production_db

Installation

go get github.com/psyb0t/gonfiguration

That’s it. Minimal deps, no bullshit setup.

How It Works

1. Define Your Config Struct

Tag your struct fields with env:"VAR_NAME" and use whatever types you need:

type UltimateConfig struct {
	// Basic types
	AppName  string `env:"APP_NAME"`
	Debug    bool   `env:"DEBUG"`
	// Integer types
	Count    int    `env:"COUNT"`
	SmallNum int8   `env:"SMALL_NUM"`
	BigNum   int64  `env:"BIG_NUM"`
	// Unsigned integers
	UserID   uint32 `env:"USER_ID"`
	FileSize uint64 `env:"FILE_SIZE"`
	// Floating point numbers
	Ratio     float32 `env:"RATIO"`
	Precision float64 `env:"PRECISION"`
	// Time durations
	Timeout   time.Duration `env:"TIMEOUT"`
	Interval  time.Duration `env:"INTERVAL"`
	// String slices
	Tags      []string `env:"TAGS"`
	Servers   []string `env:"SERVERS"`
}

2. Set Defaults

Don’t let your app crash because someone forgot an env var:

gonfiguration.SetDefaults(map[string]interface{}{
	"APP_NAME":   "MyApp",
	"DEBUG":      false,
	"COUNT":      100,
	"RATIO":      0.8,
	"TIMEOUT":    30*time.Second,
	"TAGS":       []string{"prod", "api", "web"},
	"SERVERS":    []string{"srv1.com", "srv2.com", "srv3.com"},
})

2b. Inline Defaults via Struct Tags

You can also bake defaults directly into your struct with the default tag — cleaner when the default lives with the field definition:

type Config struct {
    Port    int     `env:"PORT"    default:"8080"`
    Debug   bool    `env:"DEBUG"   default:"false"`
    Name    string  `env:"NAME"    default:"myapp"`
    Rate    float64 `env:"RATE"    default:"0.75"`
    Timeout time.Duration `env:"TIMEOUT" default:"30s"`
}
cfg := Config{}
gonfiguration.Parse(&cfg)
// cfg.Port == 8080, cfg.Name == "myapp", etc. if env vars not set

Priority: env var > programmatic default (SetDefault/SetDefaults) > struct tag default.

3. Parse It

cfg := UltimateConfig{}
if err := gonfiguration.Parse(&cfg); err != nil {
	log.Fatalf("Config parsing shit the bed: %v", err)
}

4. Done

Your config is loaded. Use it however you want.

Required Fields

Slap ,required on the env tag and the parser will fail loud when the value isn’t set anywhere — no env var, no SetDefault, no default tag. Perfect for things you absolutely cannot start without, like DB passwords or API tokens:

type Config struct {
    DBPass string `env:"DB_PASS,required"`
    APIKey string `env:"API_KEY,required"`
    // A default tag satisfies required too:
    DevKey string `env:"DEV_KEY,required" default:"dev-fallback"`
}
cfg := Config{}
if err := gonfiguration.Parse(&cfg); err != nil {
    // err is errors.Is(err, gonfiguration.ErrRequiredFieldNotSet)
    log.Fatalf("missing required config: %v", err)
}

MustParse for init code

If you’d rather have the program crash hard at startup than hand-roll an if err != nil { log.Fatal } dance, use MustParse. Same signature, panics on failure:

var cfg Config
gonfiguration.MustParse(&cfg)

Pair it with ,required tags and your init() is two lines: a struct and a MustParse. If anything’s wrong, the binary refuses to start and tells you exactly what’s missing.

String Slices Actually Work

Set a comma-separated env var and get a proper []string:

type Config struct {
	Microservices []string `env:"MICROSERVICES"`
	APIKeys       []string `env:"API_KEYS"`
}
os.Setenv("MICROSERVICES", "auth-service, user-service , payment-service,  notification-service")
os.Setenv("API_KEYS", "key1,key2,key3")
cfg := Config{}
gonfiguration.Parse(&cfg)
// You get:
// cfg.Microservices = ["auth-service", "user-service", "payment-service", "notification-service"]
// cfg.APIKeys = ["key1", "key2", "key3"]

Empty strings become empty slices:

os.Setenv("OPTIONAL_FEATURES", "")
// Results in: cfg.OptionalFeatures = []string{}

Time Durations Don’t Suck

Use Go’s duration strings and get actual time.Duration values:

type TimingConfig struct {
	HTTPTimeout    time.Duration `env:"HTTP_TIMEOUT"`
	RetryInterval  time.Duration `env:"RETRY_INTERVAL"`
	CacheExpiry    time.Duration `env:"CACHE_EXPIRY"`
}
os.Setenv("HTTP_TIMEOUT", "30s")
os.Setenv("RETRY_INTERVAL", "5m30s")
os.Setenv("CACHE_EXPIRY", "24h")
cfg := TimingConfig{}
gonfiguration.Parse(&cfg)
// You get actual durations:
// cfg.HTTPTimeout = 30 * time.Second
// cfg.RetryInterval = 5*time.Minute + 30*time.Second
// cfg.CacheExpiry = 24 * time.Hour

Debugging Your Config

Get all the values if you need to debug something:

allValues := gonfiguration.GetAllValues()
fmt.Printf("All config values: %+vn", allValues)
defaults := gonfiguration.GetDefaults()
fmt.Printf("Default values: %+vn", defaults)
envVars := gonfiguration.GetEnvVars()
fmt.Printf("Environment variables: %+vn", envVars)
// Reset everything if needed
gonfiguration.Reset()

It’s thread-safe, so concurrent access won’t fuck things up:

// Won't break
go func() {
	gonfiguration.SetDefault("KEY1", "value1")
}()
go func() {
	cfg := MyConfig{}
	gonfiguration.Parse(&cfg)
}()

When Things Go Wrong

You get actual error messages instead of cryptic bullshit:

// Forgot the pointer?
err := gonfiguration.Parse(cfg) // Should be &cfg
// Error: "invalid destination: destination must be a pointer"
// Wrong type in env var?
os.Setenv("PORT", "not-a-number")
err := gonfiguration.Parse(&cfg)
// Error: "failed to parse fields: failed to set field value: failed to parse int: strconv.ParseInt: parsing "not-a-number": invalid syntax"

Real Example

Web service config that doesn’t suck:

type WebServiceConfig struct {
	// Server configuration
	ListenAddress string        `env:"LISTEN_ADDRESS"`
	Port          int           `env:"PORT"`
	ReadTimeout   time.Duration `env:"READ_TIMEOUT"`
	WriteTimeout  time.Duration `env:"WRITE_TIMEOUT"`
	// Security
	AllowedOrigins []string `env:"ALLOWED_ORIGINS"`
	APIKeys        []string `env:"API_KEYS"`
	RateLimitRPS   int      `env:"RATE_LIMIT_RPS"`
	// Database
	DBHost     string `env:"DB_HOST"`
	DBPort     int    `env:"DB_PORT"`
	DBUser     string `env:"DB_USER"`
	DBPassword string `env:"DB_PASSWORD"`
	DBName     string `env:"DB_NAME"`
	// Redis cache
	RedisURL     string        `env:"REDIS_URL"`
	CacheExpiry  time.Duration `env:"CACHE_EXPIRY"`
	// Logging
	LogLevel     string   `env:"LOG_LEVEL"`
	LogOutputs   []string `env:"LOG_OUTPUTS"`
	// Feature flags
	EnableMetrics bool `env:"ENABLE_METRICS"`
	EnableTracing bool `env:"ENABLE_TRACING"`
}
func main() {
	cfg := WebServiceConfig{}
	gonfiguration.SetDefaults(map[string]interface{}{
		"LISTEN_ADDRESS":  "0.0.0.0",
		"PORT":           8080,
		"READ_TIMEOUT":   30*time.Second,
		"WRITE_TIMEOUT":  30*time.Second,
		"ALLOWED_ORIGINS": []string{"https://myapp.com", "https://admin.myapp.com"},
		"RATE_LIMIT_RPS": 100,
		"DB_HOST":        "localhost",
		"DB_PORT":        5432,
		"CACHE_EXPIRY":   1*time.Hour,
		"LOG_LEVEL":      "info",
		"LOG_OUTPUTS":    []string{"stdout", "file"},
		"ENABLE_METRICS": true,
		"ENABLE_TRACING": false,
	})
	if err := gonfiguration.Parse(&cfg); err != nil {
		log.Fatalf("Config fucked up: %v", err)
	}
	// Go build your app
	startWebService(cfg)
}

v1.6.0: Errors That Tell You Where They Came From

The old failure mode was a bare string. Something like “required field not set” — great, which field, set by what, called from where? Go trace it back by hand.

Every error now comes from ctxerrors instead of fmt.Errorf — across Parse, the field walker, and every single type setter. So a parse failure names the exact field, the exact setter that rejected it, and every hop it got wrapped at:

failed to parse fields: failed to set field value: field API_KEY: required
field not set [gonfiguration.go:169 in fillFieldValue] [gonfiguration.go:112
in parseDstFields] [gonfiguration.go:44 in Parse]

It’s a feat! because the error strings changed shape, but nothing you’d realistically depend on broke. The exported sentinels in errors.go are untouched and still declared with errors.New, so errors.Is matching works exactly like it did before, straight through the added wrapping. If you’re comparing with errors.Is — and you should be — you change nothing.

If you’re string-matching on error text, well. That was already a bad idea and now it’s a bad idea that broke.

That’s It

If you’re still manually parsing environment variables in 2025, use this instead.

GitHubgithub.com/psyb0t/gonfiguration

Post updated: 11 Sept 2025