Skip to content
PacoDwPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Config

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

Install

go get github.com/PacoDw/config

Quick Start

package 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.


Priority Hierarchy

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.


Struct Tags Reference

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 with default or global — the field would always have a value, making the validation meaningless. The library rejects this at unmarshal time.


Features

Nested Structs & Global Propagation

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=production

Result: 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).

Config File Support

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: secret
cfg := config.New() // reads ./.env.yaml by default

Any env var you set will override the file value for that field.

Multi-Environment Files

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

Validation

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.

Marshal (Write Config)

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.yaml

Custom Tag Names

Use 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.Getenv override is disabled to avoid accidental collisions with unrelated OS variables (e.g. a mapstructure:"user" tag matching the USER env var).

Smart Field Matching

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.


Configuration Options

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
)

Accessing Viper Directly

For advanced use cases (watching file changes, binding pflags, etc.):

cfg := config.New()
v := cfg.Viper() // *viper.Viper

Complete Example

package 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: 9090

And 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)

File Format Support

Built on Viper, so all Viper formats are supported:

YAML · JSON · TOML · HCL · Properties · ENV

See Viper's documentation for the full list.

License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages