How do you manage configuration in a production Go service?

5 minintermediategoconfigurationproductionenvironment-variables

Quick Answer

Most production Go services layer configuration from multiple sources with a clear precedence order: environment variables (the most common source for containerized deployments, read via os.Getenv or a library like envconfig/viper), command-line flags (via the standard flag package, often for local development overrides), and sometimes a config file (YAML/JSON/TOML) for larger, more structured settings. A common pattern defines a single Config struct, populates it from whichever sources apply (often flags/env overriding file defaults), and validates it once at startup — failing fast with a clear error if anything required is missing, rather than discovering a missing setting deep into a request path at runtime.

Detailed Answer

Go has no single blessed configuration approach the way some frameworks in other languages do, but a few patterns are common and idiomatic.

A typical Config struct, populated from environment variables

type Config struct {
    Port        int
    DatabaseURL string
    LogLevel    string
}

func LoadConfig() (*Config, error) {
    port, err := strconv.Atoi(getEnv("PORT", "8080"))
    if err != nil {
        return nil, fmt.Errorf("invalid PORT: %w", err)
    }
    cfg := &Config{
        Port:        port,
        DatabaseURL: os.Getenv("DATABASE_URL"),
        LogLevel:    getEnv("LOG_LEVEL", "info"),
    }
    if cfg.DatabaseURL == "" {
        return nil, fmt.Errorf("DATABASE_URL is required")
    }
    return cfg, nil
}

func getEnv(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}

Failing fast at startup

func main() {
    cfg, err := LoadConfig()
    if err != nil {
        log.Fatalf("invalid configuration: %v", err)  // fail immediately, not mid-request
    }
    // ... start server using cfg ...
}

Discovering a missing required setting the first time a specific request path is hit, minutes or hours after deployment, is a far worse operational experience than the process refusing to start at all with a clear error message.

Command-line flags for local development

port := flag.Int("port", 8080, "server port")
flag.Parse()

Flags are common for quick local overrides during development, though many production deployments rely primarily on environment variables since that's what container orchestrators (Kubernetes, ECS) naturally inject.

Structured config files for larger settings

# config.yaml
database:
  host: localhost
  port: 5432
  pool_size: 10
logging:
  level: info
  format: json

For services with many structured settings, a YAML/TOML file parsed into a Config struct (often via viper or plain encoding/yaml) can be clearer than dozens of flat environment variables, sometimes combined with environment variables for secrets/environment-specific overrides on top of file-based defaults.

The general principle

Whatever combination of sources you use, validate the fully assembled configuration once, at startup, and fail immediately and loudly if anything required is missing or invalid.