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:
cfg— routing & naming:cfg:"base_name,source:override,...". The base name is what each source looks up (the env source readsHOSTfromhost); asource:overridepins a specific key for one source.gos— behavior:gos:"default:8080,secret,optional,sep:;"— a literal default, a masked-secret flag, an unresolved-is-ok flag, and a slice separator.
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 justcfg+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.
go get github.com/goreflect/gostructorThat 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 sourceLive-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 storeRequires Go 1.24+.
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.
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 . |
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.
The docs/ directory holds the deeper reference — dip in when you need it:
- configuration.md — the
ConfigureAPI,WithSources, the fullcfg/gostag grammar, hooks, and logging. - sources.md — every source in detail, priority, and
writing your own
Source. - field-types.md — supported field types and strict conversion rules.
- observability.md — the resolution trace
(
ConfigureWithReport) and secret masking. - limitations.md — known limitations and the error taxonomy.
- migration.md — v1.0 changes and migrating from v0.x.
See ROADMAP.md for the full plan. Headlines:
- Observability & masking — ✅ shipped: a focused resolution trace
(
ConfigureWithReport). See docs/observability.md. - Hot reload — ✅ shipped: a
Watchablesource interface and aWatchhelper that re-fills the struct (transactionally, last-known-good) when a backing source changes. See docs/live-reload.md. - Git as source of truth — ✅ shipped (
gostructor/git): a branch/tag as a config version, snapshotted and polled for drift, switchable at runtime. - Config-server adapters — ✅ shipped: Consul (
gostructor/consul), etcd (gostructor/etcd), Spring Cloud Config (gostructor/springcloud), and a now-WatchableVault — each aSourcemodule with a docker-compose example. - Ergonomics — in-memory override source (
gostructor.Map) shipped; self-documenting config, whole-struct validation, and custom time layouts next.
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 + testTest 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/) -coverEach 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/.
See CONTRIBUTING.md.
