go-config-kit
A small configuration library for Go that keeps resolution explicit without repeating the same parsing code.
Configuration should be simple.
In practice, we often found ourselves choosing between a framework that hid most of the work or writing the same parsing code over and over again.
We built go-config-kit to sit somewhere in the middle.
The idea
A configuration value starts with a source.
config.Env("PORT")
We can then say how that value should be interpreted:
config.Env("PORT").As[int]()
and where it should go:
config.Env("PORT").As[int]().Into(&cfg.Port)
There is no struct scanning. There is no separate configuration language.
The code says what it is doing.
Example
A small application might have a port, an optional timeout and a database URL.
package settings
import (
"time"
"github.com/liddiard-research/go-config-kit"
)
type Config struct {
Port int
Timeout time.Duration
DatabaseURL string
}
func Load() (Config, error) {
cfg := Config{
Timeout: 5 * time.Second,
}
err := config.Pack(
config.Env("PORT").
As[int]().
Into(&cfg.Port),
config.Env("TIMEOUT").
Optional().
As[time.Duration]().
Into(&cfg.Timeout),
config.OneOfGroup(
"database URL",
config.Env("DATABASE_URL").
Into(&cfg.DatabaseURL),
config.Env("LEGACY_DATABASE_URL").
Into(&cfg.DatabaseURL),
),
)
return cfg, err
}
PORT is required and must contain an integer.
TIMEOUT is optional. If it is missing, the existing five second default is left alone.
The database URL may come from either DATABASE_URL or LEGACY_DATABASE_URL, but not both.
Pack runs each resolution and returns the errors it finds rather than stopping at the first bad value.
The result is still a normal application struct. The library does not own it.
Why we built it
Configuration code had started to feel harder than it should be.
One option was to use a larger configuration framework. That usually meant learning its tags, its precedence rules and the point at which values were actually bound.
The other option was to write everything ourselves.
value := os.Getenv("PORT")
if value == "" {
return errors.New("PORT is required")
}
port, err := strconv.Atoi(value)
if err != nil {
return err
}
cfg.Port = port
None of that code is difficult, but it gets repeated.
The useful part of the configuration gets buried under the mechanics of reading strings and converting them.
We wanted the application code to stay explicit without having to keep rewriting that plumbing.
Not another API
go-config-kit is intentionally small.
A source resolves a value. Conversion turns it into the required Go type. Pack runs the work and collects errors.
The application still decides what its configuration means.
That is the main goal of the library: remove the repetitive parts without hiding the decisions.
The GitHub README covers installation, supported conversions, JSON values and the current API.