示例说明
Viper 把配置合并的优先级内化:显式 Set 最高、环境变量次之、配置文件、最后默认值。SetEnvKeyReplacer 让 database.url 这样的嵌套键映射到 SRV_DATABASE_URL——环境变量扁平命名与配置文件的层级结构自动对上。文件缺失按设计降级而非报错,本地开发可以只靠环境变量与默认值跑起来。
技术要点
- 前缀与键名替换映射环境变量
- 配置文件缺失时优雅降级
- 加载后显式类型解析与校验
代码解读
loadConfig 依次设置文件源、环境变量前缀与替换器、四组默认值;ReadInConfig 区分文件不存在与文件损坏。GetString 拿到的超时是字符串,显式 ParseDuration 转类型——Viper 的自动类型转换对 duration 不可靠。端口范围校验兜住最后一道。
使用建议
环境变量是运维覆盖的正道,禁止在文件里提交生产密钥。校验失败快速退出优于带病运行。结构体映射可用 viper.Unmarshal 前先确认 tag。
代码示例
package main
import (
"fmt"
"strings"
"time"
"github.com/spf13/viper"
)
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
DatabaseURL string
ReplicaHosts []string
}
// precedence: flags > env > config file > defaults
func loadConfig(path string) (*ServerConfig, error) {
v := viper.New()
v.SetConfigName("config")
v.SetConfigType("yaml")
v.SetConfigFile(path)
v.SetEnvPrefix("SRV")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
v.SetDefault("host", "0.0.0.0")
v.SetDefault("port", 8080)
v.SetDefault("timeout", "5s")
v.SetDefault("replicaHosts", []string{})
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("read config: %w", err)
}
// 配置文件缺失不算错:全部回退到环境变量与默认值
}
if err := v.Validate(); err != nil {
return nil, fmt.Errorf("validate: %w", err)
}
cfg := &ServerConfig{
Host: v.GetString("host"),
Port: v.GetInt("port"),
DatabaseURL: v.GetString("database.url"),
}
if raw := v.GetString("timeout"); raw != "" {
d, err := time.ParseDuration(raw)
if err != nil {
return nil, fmt.Errorf("invalid timeout %q: %w", raw, err)
}
cfg.Timeout = d
}
if replicas := v.GetStringSlice("replicaHosts"); len(replicas) > 0 {
cfg.ReplicaHosts = replicas
}
if cfg.Port < 1 || cfg.Port > 65535 {
return nil, fmt.Errorf("port %d out of range", cfg.Port)
}
return cfg, nil
}
func main() {
cfg, err := loadConfig("configs/config.yaml")
fmt.Println(cfg, err)
}