Adder reads YAML into Go structs and overlays env vars. It does case-insensitive key matching only — it does not fold snake_case ↔ CamelCase. Every rule below exists to avoid fields silently binding to their zero value.
-
Prefer YAML keys that already match the lowercased Go field name. Adder looks up strings.ToLower(field.Name), so a field SessionTtl is resolved against the YAML key sessionttl. Both lowercase and camelCase keys work because adder lowercases both sides — camelCase is just easier to read.
auth:
username: admin
sessionTtl: 24h
type AuthConfig struct {
Username string
SessionTtl time.Duration
}
Idiomatic Go vs. tag-free binding is a tension. Go style says initialisms should be all-caps (SessionTTL). But strings.ToLower("SessionTTL") is "sessionttl", which forces either an ugly YAML key (sessionttl: 24h) or a mapstructure tag. Pick your poison: idiomatic Go + tag, or non-idiomatic field name + no tag. This skill leans toward the latter to minimize tags, but either is defensible — be consistent within a project.
This applies recursively at every nesting level — auth.session.ttl resolves each segment the same way.
-
Prefer not to use snake_case in YAML. A snake_case key like session_ttl will never bind to SessionTTL automatically — adder compares sessionttl to session_ttl and finds no match, so the field silently stays at its zero value (e.g. time.Duration(0), which means "expires immediately" for TTLs). If you must use snake_case for readability, every such field requires an explicit mapstructure tag:
SessionTTL time.Duration `mapstructure:"session_ttl"`
Prefer rule 1 over this — fewer tags, fewer ways to forget one.
-
Put defaults in application.yml, not in code. application.yml is the canonical source of default values. Code should not paper over missing config with hard-coded fallbacks (e.g. if cfg.SessionTTL == 0 { cfg.SessionTTL = 24*time.Hour }) — that hides genuine misconfiguration. If a value is required, validate at startup and panic with a clear message.
auth:
sessionTtl: 24h
-
Rely on AutomaticEnv() for the standard env var pattern. A keyPath is the dotted path adder uses internally — struct field Auth.SessionTtl becomes keyPath auth.sessionttl. With SetEnvKeyReplacer(strings.NewReplacer(".", "_")) and AutomaticEnv(), adder derives the env var as strings.ToUpper(keyPath) with dots replaced by underscores. Don't add BindEnv calls for env vars that already follow this pattern — they're noise.
keyPath: openrouter.apikey → env: OPENROUTER_APIKEY (auto, no BindEnv needed)
keyPath: db.url → env: DB_URL (auto)
-
Use BindEnv only when the env var name diverges from the auto pattern. Common reason: an established external env name (e.g. OPENROUTER_API_KEY with an underscore between API and KEY) doesn't match the auto-derived OPENROUTER_APIKEY.
_ = adder.BindEnv("openrouter.apikey", "OPENROUTER_API_KEY")
If you can rename the env var to match the auto pattern, do that instead and drop the BindEnv.
-
Co-locate config structs with the package they configure. Each package owns its own config type (internal/auth/config.go → auth.Config, internal/db/config.go → db.Config). The cmd-level Config is just composition.
type Config struct {
Auth auth.Config
DB db.Config
}
Adder binds either way — this is for code organization: package owns its fields, masking tags, and any Enabled()/Validate() helpers, and cmd/<app>/config.go stays short. Config types used only by cmd wiring (e.g. LogConfig) can stay in cmd/<app>/.
After adding or changing a config field (these checks apply at every nesting level — verify the deepest field, not just the top-level struct):