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.
GitHub: github.com/psyb0t/gonfiguration
Post updated: 11 Sept 2025