
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 reads HOST from
host); a source:override pins 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/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+.
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:
Configure API,
WithSources, the full cfg/gos tag grammar, hooks, and logging.Source.ConfigureWithReport) and secret masking.See ROADMAP.md for the full plan. Headlines:
ConfigureWithReport). See docs/observability.md.Watchable source interface and a Watch
helper that re-fills the struct (transactionally, last-known-good) when a
backing source changes. See docs/live-reload.md.gostructor/git): a branch/tag as a
config version, snapshotted and polled for drift, switchable at runtime.gostructor/consul), etcd
(gostructor/etcd), Spring Cloud Config (gostructor/springcloud), and a
now-Watchable Vault — each a Source module with a docker-compose example.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 + 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/.
See CONTRIBUTING.md.