I’ve Built the Most Ergonomic Go Config Library

Hey everyone! I just released zerocfg, the Go config library I believe to be the most ergonomic you can use. When I say “most ergonomic,” I mean it fixes long-standing pain points in the traditional Go config workflow. Let me walk you through the problems I encountered—and how zerocfg solves them. The Problems with the Standard Go Config Approach Most Go projects handle configuration roughly the same way—whether you use viper, env, confita, or another library: Define a struct for your config. Nest structs for hierarchical settings. Tag fields with metadata (e.g. yaml:"token", env:"TOKEN", etc.). Write a Parse function somewhere to set defaults, read files/env/flags, and validate. Sound familiar? Here’s what bugs me about that: 1. Boilerplate & Three Sources of Truth Every time you add a new option, you have to: Add a field in a struct—plus a tag (and sometimes even a new nested struct). In another place, declare its default value. In yet another place, pass that value into your application code. When logically related lines of code live far apart, it’s a recipe for mistakes: Typos in tags can silently break behavior, especially if defaults cover up the mistake. Renamed keys that aren’t updated everywhere will blow up in production. Extra work to add an option discourages developers—so many options go unexposed or hardcoded. 2. Configuration Sprawl Over time, your config grows unmaintained: Unused options that nobody pruned. Missing defaults that nobody set. Both should be caught automatically by a great config library. Inspiration: The Simplicity of flag The standard flag package in Go gets it right for CLI flags: var dbHost = flag.String("db_host", "localhost", "database host") func main() { flag.Parse() fmt.Println(*dbHost) } One line per option: key, default value, and description all in one place. One flag.Parse() call in main. Zero boilerplate beyond that. Why can’t we have that level of simplicity for YAML, ENV, and CLI configs? It turns out no existing library nails it—so I built zerocfg. Introducing zerocfg — Config Without the Overhead Zerocfg brings the flag package philosophy to YAML, ENV, and CLI sources, with extra sugar and flexibility. Quickstart Example package main import ( "fmt" zfg "github.com/chaindead/zerocfg" "github.com/chaindead/zerocfg/env" "github.com/chaindead/zerocfg/yaml" ) var ( path = zfg.Str("config.path", "", "path to config file", zfg.Alias("c")) host = zfg.Str("db.host", "localhost", "database host") ) func main() { if err := zfg.Parse( env.New(), // environment variables yaml.New(path), // YAML file (path comes from env or CLI) ); err != nil { panic(err) } fmt.Println("Current configuration:\n", zfg.Show()) // CMD: go run ./... -c config.yml // OUTPUT: // Current configuration: // db.host = localhost (database host) } What You Get Out of the Box Single Source of Truth Each option lives on one line: name, default, description, and any modifiers. var retries = zfg.Int("http.retries", 3, "number of HTTP retries") Pluggable & Prioritized Sources Combine any number of sources, in order of priority: zfg.Parse( yaml.New(highPriorityConfig), yaml.New(lowPriorityConfig), env.New(), ) CLI flags are always included by default at highest priority. Early Detection of Unknown Keys zfg.Parse will error on unrecognized options: err := zfg.Parse(env.New(), yaml.New(path)) if unknown, ok := zfg.IsUnknown(err); ok { fmt.Println("Unknown options:", unknown) } else if err != nil { panic(err) } Self-Documenting Config Every option has a description string. Call zfg.Show() to print a formatted config with descriptions. Option Modifiers Mark options as required, secret, give aliases, and more: password := zfg.Str("db.password", "", "database password", zfg.Secret(), zfg.Required()) Easy Extensibility Custom sources: implement a simple interface to load from anything (e.g., Consul, Vault). Custom option types: define your own zfg.Type to parse special values. Why Bother? I know plenty of us are happy with viper or env—but every project I’ve touched suffered from boilerplate, sneaky typos, and config debt. Zerocfg is my attempt to bring clarity and simplicity back to configuration. Give it a try, critique it, suggest features, or even contribute! I’d love to hear your feedback and see zerocfg grow with the community. — Enjoy, and happy coding!

May 3, 2025 - 19:55
 0
I’ve Built the Most Ergonomic Go Config Library

Hey everyone! I just released zerocfg, the Go config library I believe to be the most ergonomic you can use. When I say “most ergonomic,” I mean it fixes long-standing pain points in the traditional Go config workflow. Let me walk you through the problems I encountered—and how zerocfg solves them.

The Problems with the Standard Go Config Approach

Most Go projects handle configuration roughly the same way—whether you use viper, env, confita, or another library:

  1. Define a struct for your config.
  2. Nest structs for hierarchical settings.
  3. Tag fields with metadata (e.g. yaml:"token", env:"TOKEN", etc.).
  4. Write a Parse function somewhere to set defaults, read files/env/flags, and validate.

Sound familiar? Here’s what bugs me about that:

1. Boilerplate & Three Sources of Truth

Every time you add a new option, you have to:

  • Add a field in a struct—plus a tag (and sometimes even a new nested struct).
  • In another place, declare its default value.
  • In yet another place, pass that value into your application code.

When logically related lines of code live far apart, it’s a recipe for mistakes:

  • Typos in tags can silently break behavior, especially if defaults cover up the mistake.
  • Renamed keys that aren’t updated everywhere will blow up in production.
  • Extra work to add an option discourages developers—so many options go unexposed or hardcoded.

2. Configuration Sprawl

Over time, your config grows unmaintained:

  • Unused options that nobody pruned.
  • Missing defaults that nobody set.

Both should be caught automatically by a great config library.

Inspiration: The Simplicity of flag

The standard flag package in Go gets it right for CLI flags:

var dbHost = flag.String("db_host", "localhost", "database host")

func main() {
    flag.Parse()
    fmt.Println(*dbHost)
}
  • One line per option: key, default value, and description all in one place.
  • One flag.Parse() call in main.
  • Zero boilerplate beyond that.

Why can’t we have that level of simplicity for YAML, ENV, and CLI configs? It turns out no existing library nails it—so I built zerocfg.

Introducing zerocfg — Config Without the Overhead

Zerocfg brings the flag package philosophy to YAML, ENV, and CLI sources, with extra sugar and flexibility.

Quickstart Example

package main

import (
    "fmt"

    zfg "github.com/chaindead/zerocfg"
    "github.com/chaindead/zerocfg/env"
    "github.com/chaindead/zerocfg/yaml"
)

var (
    path = zfg.Str("config.path", "", "path to config file", zfg.Alias("c"))
    host = zfg.Str("db.host", "localhost", "database host")
)

func main() {
    if err := zfg.Parse(
        env.New(),          // environment variables
        yaml.New(path),     // YAML file (path comes from env or CLI)
    ); err != nil {
        panic(err)
    }

    fmt.Println("Current configuration:\n", zfg.Show())
    // CMD: go run ./... -c config.yml
    // OUTPUT:
    //   Current configuration:
    //     db.host = localhost  (database host)
}

What You Get Out of the Box

  1. Single Source of Truth Each option lives on one line: name, default, description, and any modifiers.
   var retries = zfg.Int("http.retries", 3, "number of HTTP retries")
  1. Pluggable & Prioritized Sources Combine any number of sources, in order of priority:
   zfg.Parse(
     yaml.New(highPriorityConfig),
     yaml.New(lowPriorityConfig),
     env.New(),
   )

CLI flags are always included by default at highest priority.

  1. Early Detection of Unknown Keys zfg.Parse will error on unrecognized options:
   err := zfg.Parse(env.New(), yaml.New(path))
   if unknown, ok := zfg.IsUnknown(err); ok {
       fmt.Println("Unknown options:", unknown)
   } else if err != nil {
       panic(err)
   }
  1. Self-Documenting Config
  • Every option has a description string.
  • Call zfg.Show() to print a formatted config with descriptions.
  1. Option Modifiers Mark options as required, secret, give aliases, and more:
   password := zfg.Str("db.password", "", "database password", zfg.Secret(), zfg.Required())
  1. Easy Extensibility
  • Custom sources: implement a simple interface to load from anything (e.g., Consul, Vault).
  • Custom option types: define your own zfg.Type to parse special values.

Why Bother?

I know plenty of us are happy with viper or env—but every project I’ve touched suffered from boilerplate, sneaky typos, and config debt. Zerocfg is my attempt to bring clarity and simplicity back to configuration.

Give it a try, critique it, suggest features, or even contribute! I’d love to hear your feedback and see zerocfg grow with the community.

— Enjoy, and happy coding!