gostructor

CI Coverage Go Report Card Go Reference License: MIT

gostructor — fill any Go struct from any source; surprises are errors

gostructor fills the fields of a Go struct from any mix of configuration sources — environment variables, files, HashiCorp Vault, or plain defaults — driven by two small struct tags, in the priority order you choose via the source list.

type Config struct {
    Host  string `cfg:"host" gos:"default:0.0.0.0"`
    Port  int    `cfg:"port" gos:"default:8080"`
    Debug bool   `cfg:"debug" gos:"default:false"`
}

cfg, err := gostructor.Configure(&Config{})

Two tags, cleanly separated:

One field, several possible sources, resolved with a priority you control (the order of the source list) — rather than merging every source into one map and unmarshalling it once. A configured field that no source can resolve is a hard error unless you mark it gos:"optional": surprises are errors.

v1.0 replaces the old cf_* tag family with just cfg + gos, makes priority the source order, and splits YAML/TOML/HOCON/Vault into their own zero-dependency-core modules. Upgrading from v0.x? See docs/migration.md.

Install

go get github.com/goreflect/gostructor

That alone gets you the env, default, JSON, and INI sources with no dependencies beyond the Go standard library. Add whichever of these you need:

go get github.com/goreflect/gostructor/yaml    # yaml source
go get github.com/goreflect/gostructor/toml    # toml source
go get github.com/goreflect/gostructor/hocon   # hocon source
go get github.com/goreflect/gostructor/vault   # vault source

Live-config and remote sources (each its own module, all optional):

go get github.com/goreflect/gostructor/watch        # fsnotify file source (hot reload)
go get github.com/goreflect/gostructor/git          # git repo as config, ref = version
go get github.com/goreflect/gostructor/consul       # Consul KV
go get github.com/goreflect/gostructor/etcd         # etcd v3
go get github.com/goreflect/gostructor/springcloud  # Spring Cloud Config Server
go get github.com/goreflect/gostructor/snapshot     # durable last-known-good store

Requires Go 1.24+.

Quick start

package main

import (
    "fmt"
    "log"

    "github.com/goreflect/gostructor"
)

type Config struct {
    Host string `cfg:"host" gos:"default:0.0.0.0"` // env HOST, else default
    Port int    `cfg:"port" gos:"default:8080"`    // env PORT, else default
}

func main() {
    cfg, err := gostructor.Configure(&Config{})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", cfg)
}

Configure reads the cfg/gos tags, tries each field’s sources in list order (the default list is Env, Default, so an env var beats the default), and returns the same pointer you passed in. A field with no gostructor tags is left untouched; a configured field that no source can resolve is a hard error (unless it’s gos:"optional"), so misconfiguration fails loudly instead of shipping a zero value.

Features & live demos

Each feature has a short page under docs/features that pairs an explanation with a runnable command and its real output — copy the command, run it against your checkout, and see the same thing.

Feature What you get Run it
Two-tag configuration Fill a struct from cfg (routing) + gos (behavior). go run ./examples/basic
Priority = source order The same struct resolving differently per environment, from the WithSources order alone. go run ./examples/priority
Sources env, default, JSON, INI (core) + YAML/TOML/HOCON/Vault (modules). go run ./examples/filesources
Field types Durations, slices, time.Time/net.IP, named types, pointers, maps, structs — strict conversion. go run ./examples/types
Hooks Validate and transform each resolved value before it lands. go run ./examples/hooks
Error taxonomy A closed set of typed errors, classified with errors.Is/errors.As. go run ./examples/errors
Observability & masking A focused resolution trace, provenance map, masked secrets. go run ./examples/observability
Full service config ~30 fields, nested sub-structs, JSON + env + defaults at once. go run ./examples/webservice
Live reload Watch re-fills the struct on change; transactional last-known-good; git/consul/etcd/springcloud/vault adapters. cd examples/hotreload-file && go run .

Supported sources

The source name is what you target in a per-source override (cfg:"port,env:DB_PORT") and what appears in the resolution trace. Non-core sources are opt-in via WithSources, so a bare cfg name never triggers an unexpected file load.

Source Reads from Module Requires
default Literal gos:"default:..." value core
env Environment variable core
json JSON file core GOSTRUCTOR_JSON=...
ini INI file core GOSTRUCTOR_INI=...
keyvalue Key/value file (.env/.properties) core GOSTRUCTOR_KEYVALUE=...
yaml YAML file gostructor/yaml GOSTRUCTOR_YAML=...
toml TOML file gostructor/toml GOSTRUCTOR_TOML=...
hocon HOCON file gostructor/hocon GOSTRUCTOR_HOCON=...
vault HashiCorp Vault secret gostructor/vault VAULT_ADDR, VAULT_TOKEN
map In-memory map (gostructor.Map) core

Live / remote sources — each implements Watchable, so gostructor.Watch re-fills the struct when the backing data changes (see docs/live-reload.md):

Source Reads from Module Live via
file Config file on disk (any format) gostructor/watch fsnotify
git File in a git repo, any format (ref = version) gostructor/git poll for drift + SetVersion
consul Consul KV prefix gostructor/consul blocking queries
etcd etcd v3 key prefix gostructor/etcd native watch API
springcloud Spring Cloud Config Server gostructor/springcloud poll
vault HashiCorp Vault secret gostructor/vault poll (secret rotation)

Each has a fully reproducible, docker-compose example under examples/. Full addressing rules, per-source key derivation, and priority: docs/sources.md.

The file and git sources are format-agnostic: they default to JSON but read any format the library supports via a pluggable decoder — pass gostructor.DecodeINI / gostructor.DecodeKeyValue (zero-dep) or yaml.Decode / toml.Decode / hocon.Decode as the source’s Decoder.

Documentation

The docs/ directory holds the deeper reference — dip in when you need it:

Roadmap

See ROADMAP.md for the full plan. Headlines:

Development

make build   # go build ./... in every module (core, yaml, toml, hocon, vault, examples/multisource)
make vet
make test    # go test ./... -race -cover in every module
make all     # build + vet + test

Test coverage (statements, library packages, excluding the runnable examples/): ~89% aggregate. Per module: core 86%, yaml 79%, toml 85%, hocon 84%, vault 87%, with the internal convert/ini/structplan parsers each above 89%. Reproduce with:

go test -coverpkg=$(go list ./... | grep -v /examples/ | paste -sd, -) \
    $(go list ./... | grep -v /examples/) -cover

Each submodule’s go.mod carries a local replace directive pointing at ../ so it builds against your working copy of core; that line is a no-op for anyone importing the module normally, since Go only applies replace directives from the main module of a build.

Runnable usage examples live in examples/.

Contributing

See CONTRIBUTING.md.

License

MIT