[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -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, &notFound) {
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.