Showcase · Configuration store · Go

config

A layered configuration store: read from files, the environment, flags and remote backends, ask where any value came from, and write a change back without wrecking the file. Rewritten off Viper to own config I/O end to end.

v0.20.1, released Pre-1.0Covers 28 projects

DocsRepository

The problem it exists for

Most configuration libraries merge every source into one big map: files, environment variables, flags, defaults. That works right up until you need to know where a value came from, because the merge throws that away. And it really goes wrong when the library writes config back by saving that merged map. Comments vanish, the order goes, defaults the user never wrote get pinned into the file, and a password that came from an environment variable lands on disk. Viper’s old write path did exactly that, and it’s the reason go/config exists.

The editing half is measurable. Across eight real config files, editing one key at a time for 70 edits in all, yamldoc changed exactly one line every time and lost no comments. The usual YAML libraries got there 24 times out of 70.

How it works

One store, and it remembers

go/config reads every source into one store, in the order you add them, and records which source supplied each value as it goes, so you can ask it to explain where a setting came from. Reads come from a frozen snapshot, so a group of related reads never mixes values from before and after a reload, and a reload that fails is rejected while the last good config stays live.

Writes go back where they came from

A change is written to the file that already owns that key, and never to an environment variable or a flag. yamldoc does the editing: it changes only the bytes of the edit, then re-reads its own output and checks it before anything is saved.

A module per format and per backend

Only YAML is built in. Everything else is an adapter of its own, so you compile only what you use: seven formats (JSON, TOML and HCL read and write; XML, dotenv, INI and Java properties read only), four filesystems, three object stores (S3, Google Cloud Storage and Azure Blob), and eleven key-value and secrets backends, from Consul, etcd and Vault to the cloud secret managers and the macOS keychain. An adapter is handed a client you’ve already built and never holds credentials of its own.

Decisions and what they cost

  • One owner for config. A single store owns all config input and output (spec 0002), instead of bolting a writer onto the old container, which would have needed locks and fingerprints only because three components shared the files. Replacing Viper had been estimated at 8,000 to 12,000 lines of work; once the store existed, the code left to replace was 781. What it cost: a breaking release, and real porting at a dozen call sites in go-tool-base. The writer I added and the Viper I deleted tells it.
  • Its own filesystem interface. A six-method interface config defines for itself, over keeping afero (23% of the dependency graph on its own), the standard library’s read-only one, or one that can’t be swapped out in tests (spec 0003). What it cost: afero users wrap it themselves, or add the adapter.
  • A module per format. One module for each format and each remote system, never a bundle (spec 0004, spec 0005), so nobody compiles every parser and cloud SDK to read one file. What it cost: twenty-seven modules to release, and every config core release becomes a release of all of them. A format per module, not a fatter config core is the argument.
  • A YAML engine of its own. yamldoc moved off the parser it was built on to an engine it owns (yamldoc spec 0008). What it cost: an edit is four to seven times slower than the plain YAML libraries (about 4 ms against 0.6 ms on a 12 KB file), and by its own docs’ account, it’s young. YAML has comments for a reason is the launch.

Proof in use

  • go-tool-base, colophon, keryx, krites and phpbotscout all read their config through it, and go-tool-base uses twenty of the adapters.
  • 385 releases across the 28 modules since July 2026, with the core at v0.20. Twenty-seven adapters releasing against one core is easy to get out of step, so every adapter’s release merge request runs cicd’s go-core-currency check, which flags a release still built against an older core.
  • yamldoc edits real config files one line at a time, measured above, in colophon and keryx as well as config itself.

Use it when, and when not to

Use it if your tool layers config from several places, needs to say where a value came from, writes changes back, or reloads while it runs.

Don’t, if you have one file, a few environment variables, no write-back and no reload: the README says plainly that Viper serves that well. It also has limits worth knowing: there are no transactions across sources, writes aren’t coordinated between processes, validation understands only required fields and enums, and a key with a dot in it can’t be reached. Explaining a value prints the value, secrets included, so mind where that output goes. yamldoc is strict, so a sloppy file fails, and its edits are slower on big files.

Where it’s going

Loading a Kubernetes ConfigMap or Secret mount as one layer per file is next, along with yamldoc’s release qualification and a few more editing operations for comments, tags and anchors.

Adapters, and the YAML editor beside it

The story in posts

Last reviewed .