Day 102 — Configuration from the Environment¶
Month 4 · Week 3 · ⬅ Day 101 · Day 103 ➡ · Journal index
🎯 Learning Objective¶
Load typed application config from environment variables with defaults, parsing, and required-value checks — and fail fast at startup with all problems at once.
📚 Topics¶
os.Getenvvsos.LookupEnv; defaults & typed parsing- Required values,
errors.Join, fail-fast; 12-factor config
📖 Reading / Sources¶
📝 Notes¶
LookupEnvbeatsGetenv.Getenvreturns""for both unset and set-to-empty;LookupEnvreturns(value, ok)so you can tell "missing" from "deliberately blank". Required-value checks need that distinction. → [[lookupenv-vs-getenv]]- Config is data from outside the binary (12-factor): never hard-code a port, DSN, or secret. Env vars keep the same image deployable to dev/stage/prod.
- Parse into typed fields, once, at boot —
strconv.Atoi,strconv.ParseBool,time.ParseDuration. Don't re-reados.Getenvdeep in handlers; load aConfigstruct and pass it down. → [[parse-dont-validate]] - Fail fast, and collect every problem. A bad deploy should report all
missing/garbage vars in one message via
errors.Join, not crash-fix-crash on each one. → [[fail-collect]] - Inject the lookup function (
func(string)(string,bool)) instead of callingos.LookupEnvinside the loader. Tests pass a map; production passesos.LookupEnv. Pure, table-testable config code. → [[dependency-injection]] - Secrets are config too, but env vars leak into logs/
/proc; for real secrets prefer a mounted file or a secret manager. Env is fine for non-secrets.
💻 Code Examples¶
type Config struct {
Port int // PORT, default 8080
Timeout time.Duration // TIMEOUT, default 5s
DatabaseURL string // DATABASE_URL, REQUIRED
}
// Inject the lookup so tests don't touch the real environment.
func Load(lookup func(string) (string, bool)) (Config, error) {
cfg := Config{Port: 8080, Timeout: 5 * time.Second}
var errs []error
if v, ok := lookup("PORT"); ok {
if n, err := strconv.Atoi(v); err != nil {
errs = append(errs, fmt.Errorf("PORT: %q is not an integer", v))
} else {
cfg.Port = n
}
}
if v, ok := lookup("DATABASE_URL"); ok && v != "" {
cfg.DatabaseURL = v
} else {
errs = append(errs, errors.New("DATABASE_URL: is required"))
}
return cfg, errors.Join(errs...) // nil when errs is empty
}
Full logic + tests:
exercises/month-04/week-3/envconfig· Run:go test ./exercises/month-04/week-3/envconfig
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Typed Load with defaults + required vars |
✅ | exercises/month-04/week-3/envconfig |
Accumulate all errors with errors.Join |
✅ | exercises/month-04/week-3/envconfig |
🐛 Mistakes Made¶
- Used
os.Getenv("DATABASE_URL") == ""to detect "missing" — couldn't tell unset from set-empty. Switched toLookupEnv'sok. - Returned on the first bad var; an operator fixed one, redeployed, hit the next.
Now
errors.Joinreports them all.
❓ Open Questions¶
- When do I graduate from env vars to a config file (YAML/TOML) + flags? (Answer:
when config grows nested/structured;
flagfor overrides, env for deploys, file for defaults — precedence flag > env > file.)
🧠 Active Recall (answer without looking)¶
-
Q: Why prefer
os.LookupEnvoveros.Getenvfor required config?
A
`LookupEnv` returns `(value, ok)`, distinguishing an *unset* variable from one *set to empty string*. `Getenv` returns `""` for both, so you can't enforce "must be present". -
Q: What does
errors.Joinreturn when you pass it zero non-nil errors?
A
`nil`. `errors.Join` ignores nil arguments and returns `nil` if every argument is nil — so `return cfg, errors.Join(errs...)` is the happy path too.
🪶 Feynman Reflection¶
Config is everything about this deployment that shouldn't be baked into the binary: which port, which database, how chatty to log. I read it from the environment once at startup, parse it into a typed struct, and if anything is missing or malformed I refuse to boot — listing every problem at once so the operator fixes them in a single pass.
🕳️ Knowledge Gaps¶
- Precedence/merging across flags + env + file, and how
flagintegrates.
✅ Summary¶
I can load typed config from the environment with defaults, parse and validate it,
distinguish unset from empty, and fail fast reporting all problems via errors.Join.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 103: per-client rate limiting with a token bucket.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦🟦⬜ |
Suggested commit: feat(exercises): typed env config with fail-fast joined errors (day 102)