How do you manage configuration in a production Go service?
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.