Read configuration values¶
Assumed setup
Every snippet below assumes a store and a ctx:
ctx := context.Background()
store, err := config.NewStore(ctx,
config.WithFiles(config.OS(), "/etc/app/config.yaml"),
config.WithEnv("APP"),
)
if err != nil {
return err
}
See Load & merge configuration for the full set of options.
Every read goes through a View, which resolves values from one immutable snapshot and
performs no I/O. Taking one is a pointer copy, so take them freely:
Read a scalar¶
host := view.GetString("server.host")
port := view.GetInt("server.port")
debug := view.GetBool("log.debug")
wait := view.GetDuration("client.timeout") // "30s", "1h15m"
An absent key returns the zero value rather than an error. If you need to tell absent from set to zero, ask:
IsSet is the same question. SectionExists("server") asks it of a subtree without
decoding one.
Read the type you actually want¶
There is a named accessor for the common widths and shapes — GetInt32, GetInt64,
GetUint through GetUint64, GetFloat64, GetTime, GetStringSlice, GetIntSlice,
GetStringMap, GetStringMapString, GetStringMapStringSlice.
For anything else, use config.Value[T], which reads any type at all:
addr, err := config.Value[netip.Addr](view, "server.bind")
hosts, err := config.Value[[]string](view, "upstream.hosts")
limits, err := config.Value[map[string]int](view, "quotas")
It is a function rather than a method because Go does not allow type parameters on
methods — the same reason UnmarshalSection and ValidateStruct are functions. It takes
a Reader, so it works against a View, an Observed handed to an observer, or a mock.
MustValue[T] is the panic-on-error variant, for package initialisation and tests.
Types that decode without you doing anything¶
Beyond the Go built-ins, these decode from their ordinary written form:
| Written as | Decodes to |
|---|---|
30s, 1h15m |
time.Duration |
10.0.0.1 |
netip.Addr, net.IP |
10.0.0.0/8 |
netip.Prefix, *net.IPNet |
10.0.0.1:8080 |
netip.AddrPort |
https://example.com |
*url.URL (pointer form) |
Europe/London |
*time.Location |
a,b,c |
[]string |
And any type of your own implementing encoding.TextUnmarshaler. That is how Go
expresses an enum or a domain type, so your types decode without this module having heard
of them:
type Severity int
const (
Debug Severity = iota
Info
)
func (s *Severity) UnmarshalText(text []byte) error {
switch string(text) {
case "debug":
*s = Debug
case "info":
*s = Info
default:
return fmt.Errorf("unknown severity %q", text)
}
return nil
}
Then read it like anything else:
The same hooks are used when decoding a struct, so a Severity field inside a
typed section works the same way.
Read a size¶
GetSizeInBytes reads values written the way an operator writes them:
Units are binary — kb, mb, gb, tb are powers of 1024. A bare number is a count of
bytes. Anything unparseable reads as 0, as the other accessors return their zero value.
Scope reads to a subtree¶
Sub returns a view rooted at a key, so a component can be handed the part of the
configuration that concerns it and nothing else:
It returns nil when the key is absent — check it. Provenance still reports the full
path, so a scoped view does not lose track of where its values came from.
Decode a whole struct¶
type AppSettings struct {
Server ServerSettings `mapstructure:"server"`
Log LogSettings `mapstructure:"log"`
}
var settings AppSettings
if err := view.Unmarshal(&settings); err != nil {
return err
}
UnmarshalKey("server", &srv) does the same for one subtree. For a typed view that
stays current across reloads, use typed sections instead.
Enumerate what is there¶
Keys returns every leaf path, sorted, including keys whose value is an empty container.
store.Snapshot().Values() returns the whole merged tree as a map — a copy, so mutating
it cannot affect the store.
Find what is set in more than one place¶
Keys tells you what exists. Shadows tells you what is set more than once, and which
copy is losing:
for _, sh := range view.Shadows() {
fmt.Printf("%s = from %s, shadowing %d\n",
sh.Path, sh.InEffect.Name, len(sh.Shadowed))
}
Only paths that more than one layer defines appear, so there is nothing to filter. It reports leaves only — a populated subtree has no single winning layer to name — and it carries paths and sources, never values.
Through a scoped view the paths are scoped too, so you can hand one straight back:
srv := view.Sub("server")
for _, sh := range srv.Shadows() {
fmt.Println(sh.Path, srv.Get(sh.Path)) // "port", 2
}
Keep a set of reads consistent¶
A View is already pinned to one snapshot, so reads through it cannot straddle a reload.
When several values must agree and you would otherwise take more than one view, pin one
explicitly:
var (
host string
port int
)
err := store.With(func(view *config.View) error {
host = view.GetString("server.host")
port = view.GetInt("server.port")
return nil
})
if err != nil {
return err
}
Related¶
- Use typed sections — decode a subtree onto your own struct
- Load & merge configuration — where the values come from
- Provenance —
Origin,Shadowed,Explain,Shadows, and why each value won - Keys and paths — what a path can and cannot address
- Struct tags — the tags decoding reads, and the types that decode from text
- Defaults and limits — the binary suffixes
GetSizeInBytesunderstands