[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# Viper Env Binding and Flag Binding
|
||||
|
||||
## The binding interaction model
|
||||
|
||||
Three settings control how viper maps environment variables to keys. They must be set together:
|
||||
|
||||
```go
|
||||
viper.SetEnvPrefix("MYAPP") // adds MYAPP_ prefix
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // database.host → MYAPP_DATABASE_HOST
|
||||
viper.AutomaticEnv() // activates auto-binding
|
||||
```
|
||||
|
||||
Call these before any `ReadInConfig` or `viper.Get*` call — typically in a root command's `PersistentPreRunE` or in `init()`.
|
||||
|
||||
## AutomaticEnv vs BindEnv
|
||||
|
||||
| Method | Behavior |
|
||||
| --- | --- |
|
||||
| `AutomaticEnv()` | Every key is automatically mapped to its env equivalent (with prefix and replacer applied) |
|
||||
| `BindEnv(key, envVars...)` | Only the specified key is bound, to the specified env var name(s) |
|
||||
|
||||
Use `AutomaticEnv` for the common case. Use `BindEnv` when you need to bind to an env var with a name that doesn't follow your prefix/replacer convention (e.g., third-party env vars like `GOOGLE_APPLICATION_CREDENTIALS`).
|
||||
|
||||
```go
|
||||
// Bind a specific non-prefixed env var
|
||||
viper.BindEnv("google.credentials", "GOOGLE_APPLICATION_CREDENTIALS")
|
||||
```
|
||||
|
||||
## SetEnvKeyReplacer in depth
|
||||
|
||||
Viper keys use `.` as separator for nested values. Env vars cannot contain dots. The replacer maps between them.
|
||||
|
||||
```go
|
||||
// Config file:
|
||||
// database:
|
||||
// host: localhost
|
||||
// max_conn: 25
|
||||
|
||||
// Without replacer:
|
||||
viper.SetEnvPrefix("MYAPP")
|
||||
viper.AutomaticEnv()
|
||||
viper.GetString("database.host") // looks for MYAPP_DATABASE.HOST — no match
|
||||
|
||||
// With replacer:
|
||||
viper.SetEnvPrefix("MYAPP")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
viper.GetString("database.host") // looks for MYAPP_DATABASE_HOST — matches
|
||||
```
|
||||
|
||||
The replacer operates on the viper key **before** prepending the prefix, so the lookup chain is: `database.host` → replace `.` with `_` → `database_host` → prepend prefix → `MYAPP_DATABASE_HOST`.
|
||||
|
||||
## AllowEmptyEnv
|
||||
|
||||
By default, viper ignores env vars set to the empty string — the empty string is treated as "not set" and viper continues down the precedence stack. Override this behavior:
|
||||
|
||||
```go
|
||||
viper.AllowEmptyEnv(true)
|
||||
// now MYAPP_PORT="" → viper.GetInt("port") == 0, not the default
|
||||
```
|
||||
|
||||
## Flag binding
|
||||
|
||||
Bind a pflag after defining it:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().Int("port", 8080, "listen port")
|
||||
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
|
||||
}
|
||||
```
|
||||
|
||||
Bind an entire flag set:
|
||||
|
||||
```go
|
||||
viper.BindPFlags(rootCmd.PersistentFlags())
|
||||
```
|
||||
|
||||
**Timing rule:** Bind flags in `init()` or in `PersistentPreRunE`. The binding call must happen before `Execute()` parses flags — specifically, before any `viper.Get*` call on a flag-backed key. Binding after `Execute()` causes the flag's `Changed` state to be unknown, so viper may not promote the flag value to the correct precedence layer.
|
||||
|
||||
## How pflag binding interacts with precedence
|
||||
|
||||
Viper checks `flag.Changed` (whether the user explicitly passed the flag). This is how it distinguishes between "flag default" (low priority) and "flag explicitly set" (high priority):
|
||||
|
||||
- `flag.Changed == false` (flag has its default): viper treats the flag as not present and falls through to env/file/default.
|
||||
- `flag.Changed == true` (flag was provided on the command line): viper treats the flag value as the highest-priority source.
|
||||
|
||||
This means `viper.GetInt("port")` correctly returns the flag value when `--port 9090` is passed, and falls back to env `MYAPP_PORT` or config file `port: 8080` otherwise.
|
||||
|
||||
## Debugging binding
|
||||
|
||||
Print all resolved values to verify your binding is correct:
|
||||
|
||||
```go
|
||||
fmt.Println(viper.AllSettings())
|
||||
// map[database:map[host:localhost max_conn:25] port:8080]
|
||||
```
|
||||
|
||||
Check env var resolution:
|
||||
|
||||
```go
|
||||
os.Setenv("MYAPP_PORT", "9090")
|
||||
viper.AutomaticEnv()
|
||||
fmt.Println(viper.GetInt("port")) // 9090
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Viper Config Sources and File Formats
|
||||
|
||||
## Supported file formats
|
||||
|
||||
Viper detects format from file extension. Supported extensions:
|
||||
|
||||
| Format | Extensions |
|
||||
| ---------- | --------------- |
|
||||
| YAML | `.yaml`, `.yml` |
|
||||
| TOML | `.toml` |
|
||||
| JSON | `.json` |
|
||||
| HCL | `.hcl` |
|
||||
| INI | `.ini` |
|
||||
| Properties | `.properties` |
|
||||
| dotenv | `.env` |
|
||||
|
||||
Force a format when there is no extension:
|
||||
|
||||
```go
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigFile("/etc/myapp/config") // no extension — type required
|
||||
```
|
||||
|
||||
## Config file search
|
||||
|
||||
```go
|
||||
viper.SetConfigName("config") // file name without extension
|
||||
viper.SetConfigType("yaml") // required when no extension
|
||||
viper.AddConfigPath("$HOME/.myapp") // search path 1 (highest priority when multiple)
|
||||
viper.AddConfigPath("/etc/myapp/") // search path 2
|
||||
viper.AddConfigPath(".") // search path 3 (lowest priority)
|
||||
|
||||
// viper searches paths in order, stops at the first match
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
var notFound *viper.ConfigFileNotFoundError
|
||||
if !errors.As(err, ¬Found) {
|
||||
return err // real error (permission denied, malformed YAML, etc.)
|
||||
}
|
||||
// not found — continue with flags/env/defaults
|
||||
}
|
||||
|
||||
// After reading, this returns the resolved path:
|
||||
fmt.Println("Using config:", viper.ConfigFileUsed())
|
||||
```
|
||||
|
||||
## Merging multiple config files
|
||||
|
||||
`MergeInConfig` merges a second config file into the current state. Later values override earlier ones for the same key.
|
||||
|
||||
```go
|
||||
viper.SetConfigFile("base.yaml")
|
||||
viper.ReadInConfig()
|
||||
|
||||
viper.SetConfigFile("override.yaml")
|
||||
viper.MergeInConfig() // keys from override.yaml win on collision
|
||||
```
|
||||
|
||||
Pattern: ship a base config with the binary, let users drop an override in `~/.myapp/override.yaml`.
|
||||
|
||||
## Multiple config files via SetConfigFile
|
||||
|
||||
For environment-based config loading:
|
||||
|
||||
```go
|
||||
env := os.Getenv("APP_ENV")
|
||||
if env == "" {
|
||||
env = "development"
|
||||
}
|
||||
viper.SetConfigFile(fmt.Sprintf("config.%s.yaml", env))
|
||||
viper.ReadInConfig()
|
||||
```
|
||||
|
||||
## Remote KV stores (etcd, Consul)
|
||||
|
||||
Viper supports remote KV stores via the `viper/remote` sub-package. This keeps remote config behind an opt-in import:
|
||||
|
||||
```go
|
||||
import _ "github.com/spf13/viper/remote"
|
||||
|
||||
// etcd
|
||||
viper.AddRemoteProvider("etcd3", "http://127.0.0.1:2379", "/config/myapp.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.ReadRemoteConfig()
|
||||
|
||||
// Consul
|
||||
viper.AddRemoteProvider("consul", "localhost:8500", "myapp/config")
|
||||
viper.SetConfigType("json")
|
||||
viper.ReadRemoteConfig()
|
||||
```
|
||||
|
||||
**Caution:** Remote config adds network latency to startup and a runtime dependency. Use it only when you need centralized config across many service instances. For most applications, files + env vars are sufficient.
|
||||
|
||||
Watch for remote changes:
|
||||
|
||||
```go
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(5 * time.Second)
|
||||
viper.WatchRemoteConfig()
|
||||
// re-read values after watching
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
## Embedding config with go:embed
|
||||
|
||||
Load config from embedded assets (for self-contained binaries):
|
||||
|
||||
```go
|
||||
//go:embed config.yaml
|
||||
var defaultConfig []byte
|
||||
|
||||
func init() {
|
||||
viper.SetConfigType("yaml")
|
||||
viper.ReadConfig(bytes.NewReader(defaultConfig))
|
||||
}
|
||||
```
|
||||
|
||||
`ReadConfig` accepts any `io.Reader`. This is useful for shipping default config inside the binary, then layering user overrides on top via `MergeInConfig`.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Viper Test Isolation
|
||||
|
||||
## The global state problem
|
||||
|
||||
The top-level `viper.*` functions operate on a global `*viper.Viper` instance shared across all tests in the same process. Tests that call `viper.SetConfigFile`, `viper.Set`, or `viper.ReadInConfig` pollute this global state, causing flaky test ordering.
|
||||
|
||||
```go
|
||||
// ✗ Bad — sets global state that affects later tests
|
||||
func TestPortConfig(t *testing.T) {
|
||||
viper.SetDefault("port", 8080)
|
||||
viper.Set("port", 9090)
|
||||
assert.Equal(t, 9090, viper.GetInt("port"))
|
||||
// global viper now has port=9090 for all subsequent tests
|
||||
}
|
||||
```
|
||||
|
||||
## viper.New() per test (correct approach)
|
||||
|
||||
```go
|
||||
func TestPortConfig(t *testing.T) {
|
||||
v := viper.New()
|
||||
v.SetDefault("port", 8080)
|
||||
v.Set("port", 9090)
|
||||
assert.Equal(t, 9090, v.GetInt("port"))
|
||||
}
|
||||
|
||||
func TestDefaultPort(t *testing.T) {
|
||||
v := viper.New()
|
||||
v.SetDefault("port", 8080)
|
||||
assert.Equal(t, 8080, v.GetInt("port")) // clean — not affected by TestPortConfig
|
||||
}
|
||||
```
|
||||
|
||||
## Injecting viper into your app
|
||||
|
||||
For test isolation to work, your application code must accept a `*viper.Viper` instead of calling the global functions directly:
|
||||
|
||||
```go
|
||||
// ✓ Good — accepts a viper instance
|
||||
type Server struct {
|
||||
cfg *viper.Viper
|
||||
}
|
||||
|
||||
func NewServer(v *viper.Viper) *Server {
|
||||
return &Server{cfg: v}
|
||||
}
|
||||
|
||||
func (s *Server) Port() int {
|
||||
return s.cfg.GetInt("port")
|
||||
}
|
||||
|
||||
// In tests:
|
||||
func TestServer(t *testing.T) {
|
||||
v := viper.New()
|
||||
v.Set("port", 9090)
|
||||
s := NewServer(v)
|
||||
assert.Equal(t, 9090, s.Port())
|
||||
}
|
||||
|
||||
// In main:
|
||||
func main() {
|
||||
// viper setup...
|
||||
s := NewServer(viper.GetViper()) // pass the global instance in production
|
||||
}
|
||||
```
|
||||
|
||||
## Reading config files in tests
|
||||
|
||||
```go
|
||||
func TestReadConfig(t *testing.T) {
|
||||
v := viper.New()
|
||||
v.SetConfigFile("testdata/config.yaml")
|
||||
require.NoError(t, v.ReadInConfig())
|
||||
assert.Equal(t, "localhost", v.GetString("host"))
|
||||
}
|
||||
```
|
||||
|
||||
Use `testdata/` for config files. Go test tooling sets the working directory to the package directory, so relative paths work reliably.
|
||||
|
||||
## t.Setenv interactions
|
||||
|
||||
`t.Setenv` sets an env var for the duration of a test and restores it on cleanup. Combined with `viper.New()` + `AutomaticEnv`, this lets you test env var binding without global pollution:
|
||||
|
||||
```go
|
||||
func TestEnvBinding(t *testing.T) {
|
||||
t.Setenv("MYAPP_PORT", "9090")
|
||||
|
||||
v := viper.New()
|
||||
v.SetEnvPrefix("MYAPP")
|
||||
v.AutomaticEnv()
|
||||
|
||||
assert.Equal(t, 9090, v.GetInt("port"))
|
||||
// t.Setenv restores original MYAPP_PORT (or unsets it) after this test
|
||||
}
|
||||
```
|
||||
|
||||
## viper.Reset() — use with caution
|
||||
|
||||
`viper.Reset()` resets the global viper instance to its zero state. It is rarely the right solution:
|
||||
|
||||
- It affects all code running concurrently that also uses the global viper.
|
||||
- It does not stop any active `WatchConfig` goroutines.
|
||||
- Using it in `TestMain` or `t.Cleanup` makes tests order-dependent.
|
||||
|
||||
Prefer `viper.New()` per test. Reserve `Reset()` for tools that call into viper-based libraries and must restore state between runs.
|
||||
|
||||
## Snapshot and restore pattern
|
||||
|
||||
When you cannot refactor to inject `*viper.Viper` and must use the global:
|
||||
|
||||
```go
|
||||
func snapshotViper() map[string]interface{} {
|
||||
return viper.AllSettings()
|
||||
}
|
||||
|
||||
func restoreViper(snapshot map[string]interface{}) {
|
||||
viper.Reset()
|
||||
for k, v := range snapshot {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithGlobalViper(t *testing.T) {
|
||||
snapshot := snapshotViper()
|
||||
t.Cleanup(func() { restoreViper(snapshot) })
|
||||
|
||||
viper.Set("port", 9090)
|
||||
// test code...
|
||||
}
|
||||
```
|
||||
|
||||
This approach is fragile — `AllSettings()` only captures the resolved values, not the binding state (defaults, env bindings, etc.). Prefer injection.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Viper Unmarshal and Struct Mapping
|
||||
|
||||
## Basic Unmarshal
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Host string `mapstructure:"host"`
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
Database struct {
|
||||
DSN string `mapstructure:"dsn"`
|
||||
MaxConn int `mapstructure:"max_conn"`
|
||||
} `mapstructure:"database"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
return fmt.Errorf("decoding config: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## mapstructure tags
|
||||
|
||||
Always use `mapstructure` tags. Without them, mapstructure falls back to case-insensitive field name matching, which works for simple cases but silently fails for:
|
||||
|
||||
- Nested structs where the outer key uses an underscore (`max_conn` → `MaxConn`)
|
||||
- Unexported fields
|
||||
- Fields where the Go name does not match the config key
|
||||
|
||||
```go
|
||||
// ✓ Good — explicit and immune to rename surprises
|
||||
type TLSConfig struct {
|
||||
CertFile string `mapstructure:"cert_file"`
|
||||
KeyFile string `mapstructure:"key_file"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
// ✗ Fragile — relies on case-folding; breaks when config key uses underscores
|
||||
type TLSConfig struct {
|
||||
CertFile string // viper key "certfile" or "CertFile", not "cert_file"
|
||||
KeyFile string
|
||||
Enabled bool
|
||||
}
|
||||
```
|
||||
|
||||
## UnmarshalKey — extracting a sub-tree
|
||||
|
||||
```go
|
||||
type DatabaseConfig struct {
|
||||
DSN string `mapstructure:"dsn"`
|
||||
MaxConn int `mapstructure:"max_conn"`
|
||||
}
|
||||
|
||||
var dbCfg DatabaseConfig
|
||||
if err := viper.UnmarshalKey("database", &dbCfg); err != nil {
|
||||
return fmt.Errorf("decoding database config: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer `UnmarshalKey` over `viper.Sub` + `Unmarshal` — fewer nil checks and less boilerplate.
|
||||
|
||||
## time.Duration
|
||||
|
||||
Viper's `GetDuration` parses duration strings (`"1h30m"`, `"500ms"`) from config files and env vars. When using `Unmarshal`, mapstructure does not know how to decode a duration string into `time.Duration` by default.
|
||||
|
||||
Register a decode hook:
|
||||
|
||||
```go
|
||||
import "github.com/mitchellh/mapstructure"
|
||||
|
||||
var cfg Config
|
||||
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
|
||||
dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
dc.DecodeHook,
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
`StringToTimeDurationHookFunc` handles `"1h30m"` → `time.Duration`. `StringToSliceHookFunc(",")` handles `"a,b,c"` → `[]string{"a", "b", "c"}`.
|
||||
|
||||
## net.IP and custom types
|
||||
|
||||
```go
|
||||
import "github.com/mitchellh/mapstructure"
|
||||
|
||||
func stringToIPHookFunc() mapstructure.DecodeHookFunc {
|
||||
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String || t != reflect.TypeOf(net.IP{}) {
|
||||
return data, nil
|
||||
}
|
||||
ip := net.ParseIP(data.(string))
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", data)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Squash for embedded structs
|
||||
|
||||
```go
|
||||
type BaseConfig struct {
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
Debug bool `mapstructure:"debug"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
BaseConfig `mapstructure:",squash"` // merge BaseConfig fields at this level
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
```
|
||||
|
||||
Without `,squash`, the base config must be nested under a `baseconfig` key in the config file.
|
||||
|
||||
## Remain for unknown keys
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Remain map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
```
|
||||
|
||||
Extra keys from the config file are collected in `Remain` instead of being silently dropped. Useful for forward compatibility.
|
||||
|
||||
## Weak decoding
|
||||
|
||||
Enable weak type decoding (e.g., string `"true"` → bool `true`) when working with env vars that are always strings:
|
||||
|
||||
```go
|
||||
var cfg Config
|
||||
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
|
||||
dc.WeaklyTypedInput = true
|
||||
})
|
||||
```
|
||||
|
||||
Use with caution — weak decoding can hide bugs where a wrong value type is silently converted.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Viper WatchConfig and Hot Reload
|
||||
|
||||
## Basic setup
|
||||
|
||||
```go
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
log.Printf("config changed: %s (op: %s)", e.Name, e.Op)
|
||||
// re-read affected values and apply them
|
||||
})
|
||||
```
|
||||
|
||||
`WatchConfig` starts a background goroutine that watches the config file using fsnotify. Call it after `ReadInConfig`.
|
||||
|
||||
## The atomic-rename trap
|
||||
|
||||
Most editors (vim, neovim, many CI tools) write config files by creating a new file and then renaming it over the old one. This replaces the inode that fsnotify is watching — the watch may not fire, or may fire with `Op: RENAME` instead of `Op: WRITE`, or may fire twice.
|
||||
|
||||
**Test hot reload with direct file writes, not editor saves:**
|
||||
|
||||
```go
|
||||
// reliable in tests:
|
||||
os.WriteFile("config.yaml", newContent, 0644)
|
||||
|
||||
// unreliable for testing:
|
||||
// opening vim and :w — may trigger RENAME instead of WRITE
|
||||
```
|
||||
|
||||
In production, this is less of an issue if your config management tool (Kubernetes ConfigMap volume mount, Consul Template, etc.) is aware of inode behavior.
|
||||
|
||||
## Race-safe reload pattern
|
||||
|
||||
Config reload happens in a background goroutine. Any shared state updated in `OnConfigChange` must be synchronized:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
MaxConn int `mapstructure:"max_conn"`
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
var newCfg Config
|
||||
if err := viper.Unmarshal(&newCfg); err != nil {
|
||||
log.Printf("error reloading config: %v", err)
|
||||
return // keep old config on error
|
||||
}
|
||||
|
||||
cfg.mu.Lock()
|
||||
cfg.LogLevel = newCfg.LogLevel
|
||||
cfg.MaxConn = newCfg.MaxConn
|
||||
cfg.mu.Unlock()
|
||||
|
||||
log.Printf("config reloaded: log_level=%s", newCfg.LogLevel)
|
||||
})
|
||||
```
|
||||
|
||||
Reads use `appCfg.mu.RLock()`. Never read directly from viper in hot paths during reload — the window between `OnConfigChange` firing and viper updating its internal state is non-deterministic.
|
||||
|
||||
## Debouncing rapid changes
|
||||
|
||||
Some filesystems fire multiple events per save. Debounce to avoid reloading multiple times:
|
||||
|
||||
```go
|
||||
var reloadTimer *time.Timer
|
||||
var reloadMu sync.Mutex
|
||||
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
reloadMu.Lock()
|
||||
defer reloadMu.Unlock()
|
||||
if reloadTimer != nil {
|
||||
reloadTimer.Stop()
|
||||
}
|
||||
reloadTimer = time.AfterFunc(100*time.Millisecond, func() {
|
||||
applyNewConfig()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Validating config before applying
|
||||
|
||||
Always validate reloaded config before applying it — an invalid config mid-reload should keep the previous working config:
|
||||
|
||||
```go
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
var candidate Config
|
||||
if err := viper.Unmarshal(&candidate); err != nil {
|
||||
log.Printf("reload: invalid config, keeping previous: %v", err)
|
||||
return
|
||||
}
|
||||
if err := validate(candidate); err != nil {
|
||||
log.Printf("reload: validation failed, keeping previous: %v", err)
|
||||
return
|
||||
}
|
||||
applyConfig(candidate)
|
||||
})
|
||||
```
|
||||
|
||||
## Stopping the watcher
|
||||
|
||||
There is no documented way to stop `WatchConfig` once started. Design your application so that the watcher's lifetime matches the process lifetime. For testing, create a new `viper.New()` instance per test — the watcher is per-instance and is garbage-collected with the instance.
|
||||
Reference in New Issue
Block a user