A declarative Go library for loading application configuration from files and environment variables into structs.
- Load configuration from multiple file paths with override logic
- Support for JSON, YAML, and TOML file formats
- Environment variable binding with prefix and nested struct support
- Automatic struct population using field tags
- Built-in support for default values and basic validation hooks
To install config-binder, use go get:
go get github.com/my-org/config-binder/binderHere's a minimal example to demonstrate config-binder's core functionality. This example is fully runnable and can be found in main.go.
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/my-org/config-binder/binder"
)
// Define your configuration structs
type DatabaseConfig struct {
Host string `default:"localhost"`
Port int `default:"5432" env:"DB_PORT"`
User string `env:"DB_USER"`
Password string `env:"DB_PASSWORD"`
Name string `env:"DB_NAME"`
SSLMode string `default:"disable"`
}
type ServerConfig struct {
Address string `default:"0.0.0.0"`
Port int `default:"8080" env:"SERVER_PORT"`
TimeoutSeconds int `default:"30"`
}
type LogConfig struct {
Level string `default:"info" env:"LOG_LEVEL"`
Format string `default:"json"`
}
type Config struct {
AppEnv string `default:"development" env:"APP_ENV"`
Debug bool `default:"false" env:"DEBUG"`
Database DatabaseConfig
Server ServerConfig
Log LogConfig
RateLimit int `default:"100"`
FeatureFlags map[string]bool
}
func main() {
fmt.Println("--- Starting config-binder example ---")
// 1. Create dummy config files for demonstration. In a real scenario, these would exist on disk.
createDummyConfigFile("config.yaml", `
appenv: production
server:
port: 9000
database:
host: my-prod-db
name: prod_app
featureflags:
new_ui: true
beta_features: false
`)
createDummyConfigFile("config.json", `
{
"server": {
"port": 9001,
"address": "127.0.0.1"
},
"database": {
"host": "my-json-db"
},
"debug": true,
"ratelimit": 150,
"featureflags": {
"new_feature": true
}
}
`)
// 2. Initialize the binder with options
// - WithPaths: Specifies files to load, in order. Later files override earlier ones.
// - WithEnvPrefix: Environment variables will be prefixed (e.g., APP_SERVER_PORT).
var cfg Config
b := binder.New(
binder.WithPaths("config.yaml", "config.json", "nonexistent.toml"), // nonexistent.toml will be skipped
binder.WithEnvPrefix("APP"),
)
// 3. Load configuration into the struct
if err := b.Load(&cfg); err != nil {
log.Fatalf("Error loading configuration: %v", err)
}
fmt.Println("\n--- Loaded Configuration (before env overrides) ---")
fmt.Printf("AppEnv: %s\n", cfg.AppEnv)
fmt.Printf("Debug: %t\n", cfg.Debug)
fmt.Printf("RateLimit: %d\n", cfg.RateLimit)
fmt.Printf("Server: %+v\n", cfg.Server)
fmt.Printf("Database: %+v\n", cfg.Database)
fmt.Printf("Log: %+v\n", cfg.Log)
fmt.Printf("FeatureFlags: %+v\n", cfg.FeatureFlags)
fmt.Println("\n--- Demonstrating Environment Variable Overrides ---")
fmt.Println("Try running with external environment variables, e.g.:")
fmt.Println(" APP_SERVER_PORT=9500 APP_DATABASE_USER=env_user APP_DEBUG=true APP_LOG_LEVEL=debug go run main.go")
// Simulate environment variables being set for demonstration purposes.
// In a real scenario, these would be set externally before running the program.
os.Setenv("APP_SERVER_PORT", "9500")
os.Setenv("APP_DATABASE_USER", "env_user")
os.Setenv("APP_DEBUG", "true")
os.Setenv("APP_LOG_LEVEL", "debug")
os.Setenv("APP_DATABASE_SSLMODE", "require") // Overrides default "disable"
// Reload config to see environment variables applied.
// For a clean demo, we re-initialize the config struct.
var cfgWithEnv Config
if err := b.Load(&cfgWithEnv); err != nil {
log.Fatalf("Error reloading configuration with env vars: %v", err)
}
fmt.Println("\n--- Loaded Configuration (with simulated env overrides) ---")
fmt.Printf("AppEnv: %s\n", cfgWithEnv.AppEnv)
fmt.Printf("Debug: %t\n", cfgWithEnv.Debug)
fmt.Printf("RateLimit: %d\n", cfgWithEnv.RateLimit)
fmt.Printf("Server: %+v\n", cfgWithEnv.Server)
fmt.Printf("Database: %+v\n", cfgWithEnv.Database)
fmt.Printf("Log: %+v\n", cfgWithEnv.Log)
fmt.Printf("FeatureFlags: %+v\n", cfgWithEnv.FeatureFlags)
// Clean up dummy files and environment variables after demo
os.Remove("config.yaml")
os.Remove("config.json")
os.Unsetenv("APP_SERVER_PORT")
os.Unsetenv("APP_DATABASE_USER")
os.Unsetenv("APP_DEBUG")
os.Unsetenv("APP_LOG_LEVEL")
os.Unsetenv("APP_DATABASE_SSLMODE")
fmt.Println("\n--- Example finished. Dummy files and env vars cleaned up ---")
}
// createDummyConfigFile creates a file with the given name and content.
// Used for demonstration purposes within main.go.
func createDummyConfigFile(filename string, content string) {
err := os.WriteFile(filename, []byte(strings.TrimSpace(content)), 0644)
if err != nil {
log.Fatalf("Failed to create dummy config file %s: %v", filename, err)
}
fmt.Printf("Created dummy config file: %s\n", filename)
}Configuration is loaded into a standard Go struct. Nested structs are fully supported.
type DatabaseConfig struct {
Host string `default:"localhost"`
Port int
// ... other database fields
}
type Config struct {
AppEnv string
Debug bool
Database DatabaseConfig // Nested struct
}Use binder.New to create a new binder instance, optionally providing binder.Option functions to customize its behavior. Then, call Load with a pointer to your configuration struct.
import "github.com/my-org/config-binder/binder"
b := binder.New(
binder.WithPaths("config.yaml", "config.json"), // Load from these files; later files override earlier ones
binder.WithEnvPrefix("MYAPP"), // Environment variables like MYAPP_DB_HOST
)
var cfg MyConfig
if err := b.Load(&cfg); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// cfg is now populatedconfig-binder automatically detects the file format (JSON, YAML, TOML) based on the file extension. Files are processed in the order they are provided, with values from later files overriding values from earlier ones. If no extension is present or it's unknown, you can explicitly set the format using binder.WithFileFormat() option:
b := binder.New(
binder.WithPaths("/etc/app/my-config"), // file without extension
binder.WithFileFormat(binder.YAML), // explicitly set format
)Fields can be mapped to environment variables using the env struct tag. By default, config-binder looks for environment variables matching the struct field name in uppercase. For nested structs, variable names are constructed by concatenating parent field names with an underscore.
Example:
type DBConfig struct {
Host string `env:"DB_HOST"` // Looks for DB_HOST
Port int // Looks for PORT (or MYAPP_DATABASE_PORT if prefix is set)
}
type Config struct {
Database DBConfig
}If binder.WithEnvPrefix("APP") is used:
Config.Database.Hostwill look forAPP_DB_HOST(theenvtag takes precedence over auto-generated names).Config.Database.Portwill look forAPP_DATABASE_PORT.
Environment variables always override values loaded from configuration files.
Provide default values using the default struct tag. These values are applied if no value is found in configuration files or environment variables.
type ServerConfig struct {
Port int `default:"8080" env:"SERVER_PORT"` // Default to 8080 if neither file nor env var provides a value
}The Load method returns an error if configuration fails to load, e.g., files not found (unless binder.WithAllowMissingFiles(true) is used), parsing errors, or type mismatches. config-binder defines custom error types in binder/errors.go for more granular error checking.
if err := b.Load(&cfg); err != nil {
log.Printf("Configuration error: %v", err)
// You can check for specific error types, e.g., errors.Is(err, binder.ErrNotFound)
}Contributions are welcome! Please open an issue or submit a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.