Maskottchen gezeichnet von @theanc1entmagusbride – Vielen Dank! 🎨
goConfy is a strongly typed, strict, and secure YAML configuration loader for Go.
It provides a complete configuration pipeline: YAML parsing → environment macro expansion → dotenv loading → profile-based overrides → strict decoding → normalization → validation → secret redaction.
- YAML parsing via
gopkg.in/yaml.v3 - Macros:
{ENV:KEY:default}and{FILE:/path:default}— exact-match, secure, no shell injection - Dotenv support: load
.envfiles without mutatingos.Environ() - Profile-based overrides:
dev,staging,prodmerged into base config - Strict decoding: rejects unknown YAML keys (catches typos)
- Typed structs: decode into
int,bool,string,time.Duration, nested structs - Normalization hook:
Normalize()called after decode - Validation hook:
Validate()called after normalization - Secret redaction:
secret:"true"tag, redaction-by-convention, and dot-path redaction - Optional tooling module:
goconfygen(CLI) andgoconfytui(TUI) are available separately in./tools
go get github.com/keksclan/goConfy@latestRequires Go 1.22+.
This installs the core runtime loader only.
Optional generator/TUI tooling lives in a separate module at ./tools.
See docs/GENERATOR.md and docs/INSTALL_TOOLS.md.
Wir folgen SemVer.
- v0.x.x: Experimentelle Phase. Breaking Changes an der API sind jederzeit möglich.
- v1.x.x: Stabile API. Wir garantieren Rückwärtskompatibilität innerhalb einer Major-Version.
Weitere Informationen zum Release-Prozess finden Sie in RELEASE.md.
package main
import (
"fmt"
"log"
goconfy "github.com/keksclan/goConfy"
"github.com/keksclan/goConfy/types"
)
type Config struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Timeout types.Duration `yaml:"timeout"`
DB struct {
DSN string `yaml:"dsn"`
Password string `yaml:"password" secret:"true"`
} `yaml:"db"`
}
func main() {
cfg, err := goconfy.Load[Config](
goconfy.WithFile("config.yml"),
)
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
fmt.Printf("Host: %s, Port: %d\n", cfg.Host, cfg.Port)
// Safe logging — secrets are replaced with [REDACTED]
json, _ := goconfy.DumpRedactedJSON(cfg)
fmt.Println(json)
}host: localhost
port: "{ENV:APP_PORT:8080}"
timeout: 30s
db:
dsn: "{ENV:DB_DSN:postgres://localhost:5432/mydb}"
password: "{ENV:DB_PASSWORD:}"APP_PORT=3000
DB_DSN=postgres://db.prod:5432/mydb
DB_PASSWORD=supersecretgo run main.goOutput:
Host: localhost, Port: 8080
{
"db": {
"dsn": "postgres://localhost:5432/mydb",
"password": "[REDACTED]"
},
"host": "localhost",
"port": 8080,
"timeout": "30s"
}
goConfy uses exact-match environment and file macros:
{ENV:KEY} → look up environment variable KEY, error if missing
{ENV:KEY:default} → look up environment variable KEY, use "default" if missing
{FILE:/path/to/file} → read entire file content (trimmed)
{FILE:/path:default} → read file content, use "default" if file missing or unreadable
- The macro must be the entire YAML value — no inline macros
- Environment Macros: Key names must be uppercase + digits + underscores:
[A-Z0-9_]+ - File Macros: The path is read directly from disk; content is trimmed of leading/trailing whitespace
- No recursive expansion — the resolved value is never re-scanned
- Security: No shell-style
${VAR}or$(cmd)— by design (see Security Model). All lookups use direct syscalls (os.Getenvoros.ReadFile), ensuring no shell injection is possible.
# ✅ Correct — entire value is a macro
port: "{ENV:PORT:8080}"
db_url: "{FILE:/run/secrets/db_url}"
fallback_cert: "{FILE:/etc/ssl/cert.pem:NONE}"
# ✅ Missing values
# For ENV: error if missing and no default
# For FILE: error if missing/unreadable and no default
host: "{ENV:HOST:localhost}"
# ❌ Will NOT expand — macro is embedded in a string
url: "http://{ENV:HOST}:8080"
# ❌ Will NOT expand — shell-style
port: "${PORT}"- No inline macros:
"prefix-{ENV:KEY}-suffix"is NOT expanded. Use separate fields or compose in your application code. - No quoting inside defaults:
{ENV:KEY:"value"}— the quotes become part of the default. Use{ENV:KEY:value}instead.
Load a .env file as an additional variable source for macro expansion. The file is parsed into memory and never injected into os.Environ().
cfg, err := goconfy.Load[Config](
goconfy.WithFile("config.yml"),
goconfy.WithDotEnvFile(".env"), // enables dotenv + sets path
)By default, OS environment wins over .env values:
goconfy.WithDotEnvOSPrecedence(true) // default — OS env wins
goconfy.WithDotEnvOSPrecedence(false) // .env wins over OS envBy default, a missing .env file causes an error:
goconfy.WithDotEnvOptional(true) // silently ignore missing .env# Comments are ignored
KEY=value
export ANOTHER_KEY=value
QUOTED="double quoted with \n escapes"
LITERAL='single quoted, no escapes'| Syntax | Result |
|---|---|
KEY=value |
value (trimmed) |
KEY="" |
empty string |
KEY=' ' |
two spaces (preserved) |
KEY="a#b" |
a#b (hash is literal in quotes) |
KEY=a #comment |
a (inline comment stripped) |
Profiles allow environment-specific overrides within a single YAML file.
host: localhost
port: 8080
profiles:
prod:
host: 0.0.0.0
port: 443cfg, err := goconfy.Load[Config](
goconfy.WithFile("config.yml"),
goconfy.WithEnableProfiles(true),
)Set the active profile:
export APP_PROFILE=prodOr explicitly:
goconfy.WithProfile("prod")- Scalars: profile value replaces base
- Mappings: merged recursively (only overridden keys change)
- Sequences: profile replaces entire list
- The
profileskey is removed before decoding
See docs/PROFILES.md for full details.
type Config struct {
Password string `yaml:"password" secret:"true"`
}
cfg := Config{Password: "secret123"}
redacted := goconfy.Redacted(cfg)
// password → "[REDACTED]"redacted := goconfy.Redacted(cfg, goconfy.WithRedactPaths([]string{"db.password"}))json, err := goconfy.DumpRedactedJSON(cfg)
// Returns JSON with all secrets replaced by "[REDACTED]"If your config struct implements Normalize(), it is called after decoding:
func (c *Config) Normalize() {
if c.Host == "" {
c.Host = "localhost"
}
c.LogLevel = strings.ToLower(c.LogLevel)
}If your config struct implements Validate() error, it is called after normalization:
func (c *Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
return nil
}goconfygen (CLI) and goconfytui (TUI) are intentionally split from the core runtime.
Install directly:
go install github.com/keksclan/goConfy/tools/cmd/goconfygen@latest
go install github.com/keksclan/goConfy/tools/cmd/goconfytui@latestOr build locally from this repository:
mkdir -p tools/bin
(cd tools && go build -o bin/goconfygen ./cmd/goconfygen)
(cd tools && go build -o bin/goconfytui ./cmd/goconfytui)Provider registry import path for generator tooling:
import "github.com/keksclan/goConfy/tools/generator/registry"See docs/GENERATOR.md for tool usage and docs/CLI.md for full CLI flags.
Demonstrates YAML loading with macros and strict decoding:
go run ./examples/basicDemonstrates .env file integration with typed decoding:
go run ./examples/dotenvDemonstrates the registry provider and goconfygen usage:
# See tools/examples/generator/README.md for details
mkdir -p tools/bin
(cd tools && go build -o bin/goconfygen ./cmd/goconfygen)Sample config and .env for exploring the TUI:
(cd tools && go run ./cmd/goconfytui)
# Then open examples/tui/config.yml and examples/tui/.env
# See tools/examples/tui/README.md for a full walkthrough| Document | Description |
|---|---|
| Getting Started | Full tutorial from zero to config |
| Generator & TUI | Optional tooling module (goconfygen, goconfytui) |
| CLI Reference | All goconfygen commands, flags, examples |
| Install Tools | Core-only vs tools installation and build commands |
| Config Tags | All supported struct tags |
| Profiles | Profile behavior and merge semantics |
| Security Model | Security design decisions |
- Go 1.22+ required
- Core dependency:
gopkg.in/yaml.v3 - Tool dependencies (only in
toolsmodule):bubbletea,lipgloss,bubbles
If you use goConfy in your project or company infrastructure, feel free to add yourself to the list:
→ See USED_BY.md
Attribution is optional but always appreciated.
This project is licensed under the MIT License. Attribution is optional but always appreciated.
See the LICENSE file for the full text.
