Introduction
Hardcoding values like ports, database URLs, and feature flags is an anti-pattern that makes deployment painful. Go applications typically read configuration from environment variables, command-line flags, or config files. Understanding these approaches lets you build twelve-factor apps that work across environments.
Key Concepts
- Environment Variables: Key-value pairs set by the operating system or container runtime. Best for secrets and deployment-specific values.
- Command-Line Flags: Arguments parsed at startup using the
flagpackage. Best for CLI tools and developer overrides. - Config Struct: A struct that aggregates all configuration in one place with typed fields and defaults.
Real World Context
Your application runs in development on your laptop, in staging on Kubernetes, and in production on a different cluster. Each environment has different database URLs, ports, and log levels. Environment variables let you configure all three without changing code.
Deep Dive
The flag and os packages cover most needs:
goimport ( "flag" "os" ) func main() { port := flag.Int("port", 8080, "server port") flag.Parse() dbHost := os.Getenv("DB_HOST") if dbHost == "" { dbHost = "localhost" // Default } }
A config struct pattern centralizes all settings:
gotype Config struct { Port int `env:"PORT" default:"8080"` DBHost string `env:"DB_HOST" required:"true"` LogLevel string `env:"LOG_LEVEL" default:"info"` }
For complex applications, libraries like viper or envconfig can read from multiple sources (env vars, files, flags) with precedence rules. But for most services, the standard library is sufficient.
A helper function pattern keeps config loading clean:
gofunc LoadConfig() *Config { return &Config{ Port: getEnvInt("PORT", 8080), Database: getEnv("DATABASE_URL", "postgres://localhost/app"), Debug: getEnvBool("DEBUG", false), } }
Common Pitfalls
- Scattering
os.Getenvcalls throughout the codebase — Centralize config reading in one place so you can see all settings at a glance. - Not providing defaults — Always have sensible defaults for development so the app runs with zero configuration.
Best Practices
- Use a single Config struct — Load all config at startup, validate it, and pass it through dependency injection.
- Fail fast on missing required config — If a required env var is missing, exit immediately with a clear error message.
Summary
- Use environment variables for deployment-specific configuration.
- Use command-line flags for CLI tools and developer overrides.
- Centralize all config in a struct with defaults.
- Validate required configuration at startup and fail fast.
Code Examples
type Config struct {
Port int
Database string
Debug bool
}
// LoadConfig reads configuration from environment variables
// with sensible defaults for local development.
func LoadConfig() *Config {
return &Config{
Port: getEnvInt("PORT", 8080),
Database: getEnv("DATABASE_URL", "postgres://localhost/app"),
Debug: getEnvBool("DEBUG", false),
}
}