Configuration Management
Production Go services load configuration from environment variables, .env files (development only), YAML/JSON files, and secret managers (AWS Secrets Manager, Vault).
Introduction
Production Go services load configuration from environment variables, .env files (development only), YAML/JSON files, and secret managers (AWS Secrets Manager, Vault). The os.Getenv function is the foundation; libraries like viper and envconfig add structure.
12-factor app principles: config in environment, not code. Validate config at startup — fail fast if required values missing. Never commit secrets; use different configs per environment (dev/staging/prod).
Interviewers ask how you manage secrets, validate config, and hot-reload settings — this lesson implements typed config loaded from env with startup validation.
The story
A Docker-deployed API reads configuration from environment variables (DATABASE_URL, LOG_LEVEL) with defaults for local development and secrets injected by Kubernetes External Secrets Operator in production. Twelve-factor app principles map directly to Go: parse config at startup, fail fast on missing required values, never reload secrets from disk in hot paths.
Tools like viper or envconfig bind env vars to structs — but even a simple os.Getenv with validation covers most microservices.
Understanding the topic
Key concepts
- os.Getenv("KEY") with default fallback pattern.
- Struct tags: envconfig, mapstructure for auto-binding.
- Load .env in dev only; production uses K8s ConfigMap/Secret.
- Validate required fields at startup before ListenAndServe.
- time.ParseDuration for config strings like "30s".
- Separate Config struct from global vars.
Step-by-step explanation
- Define Config struct with all settings.
- LoadFromEnv populates struct from environment.
- Validate checks required fields non-empty, ports in range.
- main calls LoadConfig — panic or log.Fatal on invalid.
- Pass *Config to constructors — don't reread env everywhere.
- K8s mounts secrets as env vars or files at /etc/secrets/.
Practical code example
Typed config from environment with validation at startup:
package mainimport ("fmt""os""strconv""time")type Config struct {Port intDatabaseURL stringShutdownTimeout time.DurationLogLevel string}func LoadConfig() (Config, error) {var cfg ConfigportStr := os.Getenv("PORT")if portStr == "" {portStr = "8080"}port, err := strconv.Atoi(portStr)if err != nil {return cfg, fmt.Errorf("invalid PORT: %w", err)}cfg.Port = portcfg.DatabaseURL = os.Getenv("DATABASE_URL")if cfg.DatabaseURL == "" {return cfg, fmt.Errorf("DATABASE_URL required")}timeoutStr := os.Getenv("SHUTDOWN_TIMEOUT")if timeoutStr == "" {timeoutStr = "15s"}cfg.ShutdownTimeout, err = time.ParseDuration(timeoutStr)if err != nil {return cfg, fmt.Errorf("invalid SHUTDOWN_TIMEOUT: %w", err)}cfg.LogLevel = os.Getenv("LOG_LEVEL")if cfg.LogLevel == "" {cfg.LogLevel = "info"}return cfg, nil}func main() {cfg, err := LoadConfig()if err != nil {fmt.Fprintln(os.Stderr, "config error:", err)os.Exit(1)}fmt.Printf("starting on :%d timeout=%v level=%s\n", cfg.Port, cfg.ShutdownTimeout, cfg.LogLevel)}
Line-by-line code explanation
type Config struct { Port string `env:"PORT" default:"8080"` }maps env vars to fields.port := os.Getenv("PORT")reads an environment variable — empty string if unset.if port == "" { port = "8080" }applies a safe default for local development.required := os.Getenv("DATABASE_URL")— validate non-empty and fail startup if missing.log.Fatal("DATABASE_URL required")stops the process before serving broken requests.flag.String("port", "8080", "HTTP port")adds CLI flags alongside env vars for flexibility.yaml.Unmarshalloads file-based config for local dev while prod uses env injection.never log config at INFO— secrets in DSN strings leak to log aggregators.
Key takeaway: Fail fast on missing DATABASE_URL. Defaults for non-critical settings. Pass cfg struct to server constructor.
Real-world use
Where you'll use this in production
- K8s ConfigMap for app settings, Secret for DB password.
- Feature flags toggling experimental endpoints.
- Multi-region deployment with REGION env var.
- Local dev with .env and docker-compose environment block.
Best practices
- Validate all config at startup — no silent defaults for secrets.
- Typed Config struct passed to dependencies.
- Document required env vars in README.
- Use secret manager in production, not plain env in git.
- Support config file path via flag for local override.
- Log effective config at startup minus secret values.
Common mistakes
- Reading os.Getenv scattered everywhere — inconsistent defaults.
- Committing .env with real credentials.
- No validation — nil pointer when secret missing at runtime.
- Hardcoding production URLs in source code.
- Logging full config including API keys at Info level.
Advanced interview questions
Q1BeginnerConfig source priority?
Q2Beginner12-factor config rule?
Q3IntermediateK8s secrets as files vs env?
Q4Intermediateviper vs manual env?
Q5AdvancedDesign config for multi-tenant SaaS.
Summary
Load typed Config struct from environment at startup. Validate required values; fail fast before serving traffic. Never commit secrets; use K8s Secrets or vault. Pass Config to constructors — don't scatter getenv calls. Next lesson: Dockerizing Go applications.