A Go configuration library that merges environment variables, config files, global tags, and struct defaults with a clear, predictable priority order — powered by Viper under the hood.
env vars > config file > global tag > default tag
go get github.com/PacoDw/configpackage main
import (
"log"
"github.com/PacoDw/config"
)
type AppConfig struct {
Name string `env:"APP_NAME" default:"my-app"`
Port int `env:"PORT" default:"8080"`
Debug bool `env:"DEBUG" default:"false"`
}
func main() {
cfg := config.New()
var app AppConfig
if err := cfg.Unmarshal(&app); err != nil {
log.Fatal(err)
}
log.Printf("Starting %s on :%d (debug=%v)", app.Name, app.Port, app.Debug)
}That's it. Set APP_NAME=coolservice in your environment and it overrides the default — no extra code required.
Every field is resolved through four layers, top wins:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | OS environment variable | export APP_NAME=prod-app |
| 2 | Config file value | APP_NAME: file-app in .env.yaml |
| 3 | Global tag propagation | global:"ENVIRONMENT" inherits from parent |
| 4 (lowest) | Struct default tag | default:"my-app" |
This means you can ship a config file with sane defaults for development, override individual values per environment via env vars, and still have struct-level fallbacks for anything not specified.
| Tag | Purpose | Example |
|---|---|---|
env |
Maps the field to an env var / config key | env:"DB_HOST" |
default |
Fallback value when nothing else provides one | default:"localhost" |
global |
Inherits a top-level value into nested structs | global:"ENVIRONMENT" |
validate |
Field validation (go-playground/validator) | validate:"required,min=1024" |
Rule:
validate:"required"cannot be combined withdefaultorglobal— the field would always have a value, making the validation meaningless. The library rejects this at unmarshal time.
Define your configuration as deeply nested structs. Use the global tag to propagate a top-level value (like ENVIRONMENT) into every nested struct that needs it:
type AppConfig struct {
Environment string `env:"ENVIRONMENT" default:"development"`
API APIConfig `env:"api"`
Worker WorkerConfig `env:"worker"`
}
type APIConfig struct {
Host string `env:"API_HOST" default:"0.0.0.0"`
Port int `env:"API_PORT" default:"8080"`
Environment string `env:"API_ENV" global:"ENVIRONMENT"`
}
type WorkerConfig struct {
Concurrency int `env:"WORKER_CONCURRENCY" default:"4"`
Environment string `env:"WORKER_ENV" global:"ENVIRONMENT"`
}export ENVIRONMENT=productionResult: API.Environment and Worker.Environment both resolve to "production" via the global tag, without having to set API_ENV or WORKER_ENV individually.
If you do set API_ENV=staging, it wins over the global value (env vars > global tag).
Place a .env.yaml (or .json, .toml, etc.) in your project root:
# .env.yaml
APP_NAME: my-service
ENVIRONMENT: staging
api:
API_HOST: 0.0.0.0
API_PORT: 9090
database:
DB_HOST: db.internal
DB_PORT: 5432
DB_USER: app
DB_PASSWORD: secretcfg := config.New() // reads ./.env.yaml by defaultAny env var you set will override the file value for that field.
Organize config files per environment:
myapp/
├── .env.yaml # production (default)
├── local.env.yaml # local / dev
└── stg.env.yaml # staging
env := os.Getenv("APP_ENV") // "local", "stg", "prod", etc.
cfg := config.New(config.WithEnvironment(env))| Input | File loaded |
|---|---|
"local", "dev", "development" |
local.env.yaml |
"staging", "stg" |
stg.env.yaml |
"production", "prod", "prd" |
.env.yaml |
"custom" |
custom.env.yaml |
Use any go-playground/validator tag:
type Config struct {
Email string `env:"EMAIL" validate:"required,email"`
Port int `env:"PORT" validate:"required,min=1024,max=65535"`
URL string `env:"URL" validate:"required,url"`
}Validation runs after defaults are applied, so only truly missing required fields will fail.
Persist a struct back to the config file:
app := AppConfig{Name: "new-app", Port: 9090, Debug: true}
cfg := config.New(
config.WithFilePath("./"),
config.WithFileName("output"),
config.WithFileType("yaml"),
)
if err := cfg.Marshal(&app); err != nil {
log.Fatal(err)
}
// writes ./output.env.yamlUse mapstructure, json, or any other tag instead of env:
type Config struct {
Name string `mapstructure:"app_name" default:"my-app"`
Port int `mapstructure:"port" default:"8080"`
}
cfg := config.New(config.WithTagName("mapstructure"))Note: When using a custom tag name, the direct
os.Getenvoverride is disabled to avoid accidental collisions with unrelated OS variables (e.g. amapstructure:"user"tag matching theUSERenv var).
The library automatically matches keys across naming conventions:
api_key(snake_case) →APIKey(PascalCase) ✓serverPort(camelCase) →server_port(snake_case) ✓DB_HOST(UPPER_SNAKE) →Host(PascalCase) ✓
No manual mapping needed.
cfg := config.New(
config.WithFilePath("/etc/myapp"), // Directory (default: "./")
config.WithFileName("config"), // File name without ext (default: "")
config.WithExtension(".yaml"), // Extension (default: ".env")
config.WithFileType("yaml"), // Viper format (default: "yaml")
config.WithEnvironment("stg"), // Environment-specific file
config.WithTagName("mapstructure"), // Custom struct tag (default: "env")
config.WithFullPath("/etc/app/cfg.yaml"),// Set everything from a full path
)For advanced use cases (watching file changes, binding pflags, etc.):
cfg := config.New()
v := cfg.Viper() // *viper.Viperpackage main
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/PacoDw/config"
)
type AppConfig struct {
Name string `env:"APP_NAME" default:"my-service"`
Environment string `env:"ENVIRONMENT" default:"development"`
Database DatabaseConfig `env:"database"`
API APIConfig `env:"api"`
}
type DatabaseConfig struct {
Host string `env:"DB_HOST" default:"localhost"`
Port int `env:"DB_PORT" default:"5432"`
User string `env:"DB_USER" validate:"required"`
Password string `env:"DB_PASSWORD" validate:"required"`
}
type APIConfig struct {
Host string `env:"API_HOST" default:"0.0.0.0"`
Port int `env:"API_PORT" default:"8080"`
Environment string `env:"API_ENV" global:"ENVIRONMENT"`
}
func main() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "local"
}
cfg := config.New(config.WithEnvironment(env))
var app AppConfig
if err := cfg.Unmarshal(&app); err != nil {
log.Fatalf("config error: %v", err)
}
blob, _ := json.MarshalIndent(app, "", " ")
fmt.Println(string(blob))
}With this .env.yaml:
APP_NAME: my-service
ENVIRONMENT: staging
database:
DB_HOST: db.internal
DB_PORT: 5432
DB_USER: app
DB_PASSWORD: secret
api:
API_HOST: 0.0.0.0
API_PORT: 9090And export ENVIRONMENT=production:
{
"Name": "my-service",
"Environment": "production",
"Database": {
"Host": "db.internal",
"Port": 5432,
"User": "app",
"Password": "secret"
},
"API": {
"Host": "0.0.0.0",
"Port": 9090,
"Environment": "production"
}
}Environment→"production"(env var wins over file's"staging")API.Environment→"production"(global tag propagates the winning value)Database.Host→"db.internal"(config file, no env var set)API.Port→9090(config file value)Name→"my-service"(config file matches default, env var not set)
Built on Viper, so all Viper formats are supported:
YAML · JSON · TOML · HCL · Properties · ENV
See Viper's documentation for the full list.
This project is licensed under the MIT License.