Configuration Management in Go
Managing configuration in Go applications can be done through environment variables, command-line flags, configuration files, or a combination of these. The viper library provides a unified interface for configuration management.
Environment Variables
Using environment variables for configuration:
package main
import (
"fmt"
"os"
"strconv"
)
type Config struct {
DatabaseURL string
Port int
Debug bool
Timeout int
}
func loadConfig() (*Config, error) {
config := &Config{}
// Required string value
config.DatabaseURL = os.Getenv("DATABASE_URL")
if config.DatabaseURL == "" {
return nil, fmt.Errorf("DATABASE_URL environment variable is required")
}
// Optional int value with default
portStr := os.Getenv("PORT")
if portStr == "" {
config.Port = 8080 // default
} else {
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("invalid PORT: %w", err)
}
config.Port = port
}
// Optional bool value
config.Debug = os.Getenv("DEBUG") == "true"
// Optional duration with default
timeoutStr := os.Getenv("TIMEOUT")
if timeoutStr == "" {
config.Timeout = 30 // default 30 seconds
} else {
timeout, err := strconv.Atoi(timeoutStr)
if err != nil {
return nil, fmt.Errorf("invalid TIMEOUT: %w", err)
}
config.Timeout = timeout
}
return config, nil
}
func main() {
config, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
fmt.Printf("Config: %+v\n", config)
}Command-Line Flags
Using the flag package:
package main
import (
"flag"
"fmt"
"time"
)
type Config struct {
Host string
Port int
Verbose bool
Timeout time.Duration
Database string
}
func main() {
config := &Config{}
// Define flags
flag.StringVar(&config.Host, "host", "localhost", "Server host")
flag.IntVar(&config.Port, "port", 8080, "Server port")
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose logging")
flag.DurationVar(&config.Timeout, "timeout", 30*time.Second, "Request timeout")
flag.StringVar(&config.Database, "database", "app.db", "Database file path")
// Custom flag value
var logLevel string
flag.StringVar(&logLevel, "log-level", "info", "Log level (debug, info, warn, error)")
// Parse flags
flag.Parse()
// Validate and process
switch logLevel {
case "debug", "info", "warn", "error":
// valid
default:
fmt.Printf("Invalid log level: %s\n", logLevel)
flag.Usage()
return
}
fmt.Printf("Config: %+v\n", config)
fmt.Printf("Log level: %s\n", logLevel)
// Remaining arguments
args := flag.Args()
if len(args) > 0 {
fmt.Printf("Additional arguments: %v\n", args)
}
}Configuration Files
JSON Configuration
package main
import (
"encoding/json"
"fmt"
"os"
)
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
TLS bool `json:"tls"`
CertFile string `json:"cert_file,omitempty"`
KeyFile string `json:"key_file,omitempty"`
}
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
}
type AppConfig struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
LogLevel string `json:"log_level"`
}
func loadJSONConfig(filename string) (*AppConfig, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open config file: %w", err)
}
defer file.Close()
var config AppConfig
decoder := json.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
return nil, fmt.Errorf("failed to decode config: %w", err)
}
return &config, nil
}
func main() {
config, err := loadJSONConfig("config.json")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Server Host: %s\n", config.Server.Host)
fmt.Printf("Database: %s\n", config.Database.Database)
fmt.Printf("Log Level: %s\n", config.LogLevel)
}Example config.json:
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"tls": true,
"cert_file": "server.crt",
"key_file": "server.key"
},
"database": {
"host": "localhost",
"port": 5432,
"username": "app_user",
"password": "secret_password",
"database": "app_db"
},
"log_level": "info"
}YAML Configuration
package main
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
App struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Debug bool `yaml:"debug"`
} `yaml:"app"`
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Name string `yaml:"name"`
} `yaml:"database"`
Features []string `yaml:"features"`
}
func loadYAMLConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
return &config, nil
}
func main() {
config, err := loadYAMLConfig("config.yaml")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("App Name: %s\n", config.App.Name)
fmt.Printf("Features: %v\n", config.Features)
}Example config.yaml:
app:
name: MyApp
version: 1.0.0
debug: false
database:
host: localhost
port: 5432
username: app_user
password: secret_password
name: app_db
features:
- authentication
- logging
- metricsUsing Viper for Configuration
Viper provides a unified configuration interface:
package main
import (
"fmt"
"log"
"github.com/spf13/viper"
)
type Config struct {
Server struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
} `mapstructure:"server"`
Database struct {
URL string `mapstructure:"url"`
} `mapstructure:"database"`
Features []string `mapstructure:"features"`
}
func loadConfig() (*Config, error) {
// Set config file name (without extension)
viper.SetConfigName("config")
// Add config paths
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.AddConfigPath("/etc/myapp")
// Enable reading from environment variables
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
// Set defaults
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.port", 8080)
viper.SetDefault("database.url", "postgres://localhost/myapp")
viper.SetDefault("features", []string{"basic"})
// Read config file
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Config file not found, using defaults and env vars
log.Println("Config file not found, using defaults and environment variables")
}
// Override with command line flags
viper.BindPFlag("server.port", flag.Lookup("port"))
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &config, nil
}
func main() {
// Parse command line flags first
port := flag.Int("port", 8080, "Server port")
flag.Parse()
config, err := loadConfig()
if err != nil {
log.Fatal("Failed to load config:", err)
}
fmt.Printf("Server: %s:%d\n", config.Server.Host, config.Server.Port)
fmt.Printf("Database: %s\n", config.Database.URL)
fmt.Printf("Features: %v\n", config.Features)
// Watch for config changes
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Reload configuration
})
}Configuration Validation
Validating configuration values:
package main
import (
"errors"
"fmt"
"net/url"
"regexp"
)
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
type ValidationErrors []ValidationError
func (e ValidationErrors) Error() string {
if len(e) == 0 {
return ""
}
msg := "validation errors:"
for _, err := range e {
msg += fmt.Sprintf("\n %s", err.Error())
}
return msg
}
func validateConfig(config *Config) error {
var errors ValidationErrors
// Validate server host
if config.Server.Host == "" {
errors = append(errors, ValidationError{
Field: "server.host",
Message: "cannot be empty",
})
}
// Validate server port
if config.Server.Port < 1 || config.Server.Port > 65535 {
errors = append(errors, ValidationError{
Field: "server.port",
Message: "must be between 1 and 65535",
})
}
// Validate database URL
if config.Database.URL != "" {
if _, err := url.Parse(config.Database.URL); err != nil {
errors = append(errors, ValidationError{
Field: "database.url",
Message: "invalid URL format",
})
}
}
// Validate features
validFeatures := map[string]bool{
"authentication": true,
"logging": true,
"metrics": true,
"caching": true,
}
for _, feature := range config.Features {
if !validFeatures[feature] {
errors = append(errors, ValidationError{
Field: "features",
Message: fmt.Sprintf("unknown feature: %s", feature),
})
}
}
if len(errors) > 0 {
return errors
}
return nil
}
func main() {
config := &Config{
Server: struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}{
Host: "",
Port: 99999,
},
Database: struct {
URL string `mapstructure:"url"`
}{
URL: "invalid-url",
},
Features: []string{"authentication", "invalid_feature"},
}
if err := validateConfig(config); err != nil {
fmt.Printf("Configuration validation failed:\n%s\n", err)
} else {
fmt.Println("Configuration is valid")
}
}Best Practices
- Use multiple sources: Combine environment variables, config files, and flags
- Set sensible defaults: Don’t require every configuration option
- Validate configuration: Check values at startup
- Document configuration: Explain what each option does
- Use consistent naming: Follow conventions (snake_case for env vars, camelCase for struct fields)
- Handle missing config gracefully: Provide fallbacks
- Don’t log sensitive data: Be careful with passwords and secrets
- Support different environments: dev, staging, prod configs
- Version config files: Keep track of config changes
- Test configuration loading: Include config tests in your test suite
Effective configuration management makes your Go applications more flexible and easier to deploy across different environments.
For more on environment variables, check our environment variables tutorial. If you’re deploying applications, see the deployment tutorial.
Last updated on