Load service settings from the environment

Go · Go · beginner · modification

Moves the server's environment parsing out of `main` into a small `config` package. `config.Load` takes the lookup function, so tests pass a map instead of touching the real environment. Bad values used to fall back to the defaults without a word (a `REQUEST_TIMEOUT=30` meant 15 seconds); now the server refuses to start and logs one message listing every bad variable with what it expects. `main` gets a `run` function so the deferred `db.Close()` runs on the way out. Table-driven tests cover the defaults, each bound and the combined error.

Go 1.22 HTTP service, standard library only (`net/http`, `log/slog`). Existing and unchanged: `store.Open(url)` connects to PostgreSQL and returns an error for a URL it cannot use, and its errors never include the URL or the password; `api.Routes(db, logger, api.Options)` builds the handlers and wraps request bodies in `http.MaxBytesReader` with `Options.MaxBodyBytes`; `Options.SignupsEnabled` turns `POST /signup` on. The service runs in containers whose environment is set by the deploy config, behind a load balancer that health-checks the port.

Requirements

Files touched

--- cmd/server/main.go
 
 import (
+	"fmt"
 	"log/slog"
 	"net/http"
 	"os"
-	"strconv"
 	"time"
 
 	"example.com/accounts/internal/api"
+	"example.com/accounts/internal/config"
 	"example.com/accounts/internal/store"
 )
 func main() {
 	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
-
-	port := os.Getenv("PORT")
-	if port == "" {
-		port = "8080"
-	}
-	timeout := 15 * time.Second
-	if d, err := time.ParseDuration(os.Getenv("REQUEST_TIMEOUT")); err == nil {
-		timeout = d
-	}
-	maxBody := int64(1 << 20)
-	if n, err := strconv.ParseInt(os.Getenv("MAX_BODY_BYTES"), 10, 64); err == nil {
-		maxBody = n
-	}
-	signups := os.Getenv("SIGNUPS_ENABLED") == "true"

Review this PR

Go practice