当前位置: 首页 > news >正文

golang——viper库学习记录

前言

在实际项目开发中,一旦涉及到第三方中间件,就无可避免需要整合相关的配置信息,较典型的:MYSQL、REDIS数据库的相应配置(HOST、DATABASE、PASSWORD…)等。如果全都手动实现硬编码,的确,从运行等方面并无大问题,但着实不太方便、雅观。如下是一个“硬”配置的简例:

package mainimport ("encoding/json""flag""fmt""log""os""strconv"
)// Config 结构体定义应用配置
type Config struct {Database struct {Host     string `json:"host"`Port     int    `json:"port"`User     string `json:"user"`Password string `json:"password"`Name     string `json:"name"`} `json:"database"`Timeout int `json:"timeout"`Port    int `json:"port"`
}func main() {// 1. 定义命令行参数configPath := flag.String("config", "config.json", "配置文件路径")port := flag.Int("port", 0, "服务器端口")flag.Parse()// 2. 初始化默认配置config := Config{Timeout: 30, // 默认超时时间}// 3. 从配置文件读取if *configPath != "" {file, err := os.ReadFile(*configPath)if err != nil {log.Printf("警告: 无法读取配置文件: %v", err)} else {if err := json.Unmarshal(file, &config); err != nil {log.Fatalf("错误: 配置文件格式错误: %v", err)}}}// 4. 从环境变量覆盖配置if envHost := os.Getenv("APP_DB_HOST"); envHost != "" {config.Database.Host = envHost}if envPort := os.Getenv("APP_DB_PORT"); envPort != "" {portNum, err := strconv.Atoi(envPort)if err != nil {log.Fatalf("错误: APP_DB_PORT 不是有效整数: %v", err)}config.Database.Port = portNum}// 5. 从命令行参数覆盖配置if *port != 0 {config.Port = *port}// 6. 使用配置fmt.Printf("数据库连接: %s:%d\n", config.Database.Host, config.Database.Port)fmt.Printf("服务器端口: %d\n", config.Port)fmt.Printf("超时时间: %d秒\n", config.Timeout)
}

手动实现的局限性

  1. 格式支持有限:需要为每种配置格式(JSON、YAML 等)编写单独的解析逻辑
  2. 环境变量处理繁琐:需要手动处理类型转换和错误检查
  3. 配置变更监听缺失:无法自动检测文件变更并重新加载
  4. 缺少远程配置支持:不支持从 Consul、Etcd 等服务读取配置
  5. 代码复杂度增加:配置管理代码与业务逻辑耦合,维护成本高

而golang的viper库就为此提供了“软着陆”的方式,使得开发更加便利


什么是viper

Viper是适用于Go应用程序的完整配置解决方案,具备从多种来源读取配置的能力,包括文件(像 JSON、YAML、TOML 等格式)、环境变量、命令行参数以及远程配置系统(例如 Consul、Etcd)。借助这种多源整合功能,它能够轻松实现配置的优先级覆盖,比如命令行参数可以覆盖环境变量,而环境变量又能覆盖配置文件中的设置。


功能

  • 支持配置key默认值设置

  • 支持读取JSON,TOML,YAML,HCL,envfile和java properties等多种不同类型配置文件

  • 可以监听配置文件的变化,并重新加载配置文件

  • 读取系统环境变量的值

  • 读取存储在远程配置中心的配置数据,如ectd,Consul,firestore等系统,并监听配置的变化

  • 从命令行读取配置

  • 从buffer读取配置

  • 可以显示设置配置的值

配置优先级:

从多个数据源读取配置值,因此当同一个配置key在多个数据源有值时,viper读取的优先级如下:

  1. 显示使用Set函数设置值
  2. flag:命令行参数
  3. env:环境变量
  4. config:配置文件
  5. key/value store:key/value存储系统,如(etcd)
  6. default:默认值

在这里插入图片描述

以下为上述代码的viper实现,假如不考虑冗余的错误检测机制,它会是这样的:

package mainimport ("flag""fmt""log""strings""github.com/spf13/viper"
)// Config 配置结构体(使用 mapstructure 映射)
type Config struct {Database struct {Host     string `mapstructure:"host"`Port     int    `mapstructure:"port"`User     string `mapstructure:"user"`Password string `mapstructure:"password"`Name     string `mapstructure:"name"`} `mapstructure:"database"`Timeout int `mapstructure:"timeout"`Port    int `mapstructure:"port"`
}func main() {// 初始化 Viperv := viper.New()// 1. 命令行参数(先解析,方便指定配置文件路径)configPath := flag.String("config", "config.json", "配置文件路径")flag.IntVar(&v.GetInt("port"), "port", 0, "服务器端口") // 绑定端口参数flag.Parse()// 2. 配置文件设置v.SetConfigFile(*configPath) // 直接使用命令行指定的路径// 或者干脆直接 v.SetConfigFile("config.json"),甚至可以省掉命令行参数,但并不建议这么做v.SetConfigType("json")      // 显式指定类型(可选,Viper 可自动识别)// 3. 默认值(最低优先级)v.SetDefault("timeout", 30)v.SetDefault("database.host", "localhost")v.SetDefault("database.port", 3306) // 假设默认MySQL端口// 4. 环境变量(自动覆盖配置文件)v.AutomaticEnv()v.SetEnvPrefix("app")                  // 环境变量前缀:APP_xxxv.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // database.host → APP_DATABASE_HOST// 5. 读取配置文件(忽略"文件不存在"错误,用默认值兜底)if err := v.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); !ok {log.Fatalf("配置文件解析错误: %v", err) // 格式错误才中断}}// 6. 映射到结构体var cfg Configif err := v.Unmarshal(&cfg); err != nil {log.Fatalf("配置映射失败: %v", err)}// 7. 输出配置fmt.Printf("数据库: %s:%d\n", cfg.Database.Host, cfg.Database.Port)fmt.Printf("服务器端口: %d\n", cfg.Port)fmt.Printf("超时时间: %d秒\n", cfg.Timeout)
}

在事实上让代码的可读性提高了,形式上也更加便于维护。

为什么说它 “简洁”?

和原始代码对比,核心差异在于:

  1. 配置优先级无需手动判断
    原始代码需要手动写 默认值 → 配置文件 → 环境变量 → 命令行 的覆盖逻辑(4 处判断),而 Viper 内部自动处理了优先级,你只需要声明 “有哪些配置源”。
  2. 环境变量绑定更灵活
    原始代码需要为每个环境变量写 os.Getenv + 类型转换(如 APP_DB_PORT 的处理),Viper 一行 AutomaticEnv() 就能自动绑定所有环境变量,且支持前缀和符号转换(._)。
  3. 扩展性更好
    如果后续需要支持:
    • 多格式配置文件(yaml/toml)
    • 远程配置(etcd/consul)
    • 更多配置项(如 LogLevelMaxConnections
      Viper 只需几行代码扩展,而原始代码需要大量重复的 if-else + 类型转换

什么时候会 “更复杂”?

如果需求极其简单(类似于固定 JSON 文件 + 几个环境变量),Viper 确实会显得 “多余”。但在真实项目中,配置逻辑往往会随着需求增长而膨胀,Viper 本质是用 “初始化时的一点复杂度” 换 “长期维护的简洁”。

如果还是觉得繁琐,其实还可以再简化 —— 比如去掉结构体映射,直接用 v.GetInt("port") 获取配置,但结构体的好处是类型安全 + 自动文档化(IDE 能提示字段名)。


常用基础命令

1. 初始化与配置文件

基本配置
import "github.com/spf13/viper"func init() {viper.SetConfigName("config")  // 配置文件名(不带扩展名)viper.SetConfigType("yaml")    // 配置文件类型(yaml/json/toml/hcl等)viper.AddConfigPath(".")       // 配置文件搜索路径viper.AddConfigPath("/etc/app/")  // 可添加多个搜索路径
}
读取配置文件
if err := viper.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); ok {// 配置文件不存在} else {// 配置文件存在但读取错误}
}

对比Set与Add:

Set型:为配置文件可能存在的路径/名称,尤其以SetConfigName()为主要,亦或者SetConfigFile()。

Add型:为配置文件的搜索路径

即二者分别为:叫什么/是什么,去哪里找

  • 应当注意的一点是,同样为Set方法,SetConfigType()的存在并不见得一定会影响文件的搜索定向。

比如有以下的情况:

# 在后台同路径下,有该两种文件
config.yaml   config.json

当利用Set方法进行名称的定义时,会这么写

v.SetConfigName("config")
v.SetConfigType("yaml") // config.yaml为实际所需要的配置文件

但在搜索时会发现,其可能实际定位到了config.json,这是由于viper的默认的文件查找及优先级逻辑

  1. viper 的默认行为
    当你只设置了 v.SetConfigName("config")(未指定扩展名),viper 会在目标路径下自动搜索所有支持的格式(如 yaml、json、toml 等),并按内置优先级尝试加载第一个找到的文件。
  2. 冲突的根源
    此时 SetConfigType 的作用会被弱化 —— 因为 viper 优先根据文件实际扩展名来判断格式,而非 SetConfigType 的声明。例如:
    • 即使你调用了 v.SetConfigType("yaml"),但如果 viper 先找到 config.json,仍会优先以 json 格式加载它
    • SetConfigType 仅在 “文件无扩展名” 或 “从非文件源加载” 时才会强制生效
  3. 本质问题
    这不是 SetConfigType 的功能缺陷,而是 viper 的文件发现机制导致的 —— 当多个同名称不同格式的文件共存时,它无法仅凭 SetConfigType 来筛选目标文件,必须通过明确的文件名(含扩展名)来定位。

简单的说即是:

SetConfigType 只负责“如何解析”,并不负责“具体解析哪一个”;因此开发过程中,非必要尽量不使文件重名,或配置文件时使用SetConfigFile直接指定对应文件

2. 获取配置值

基本类型
viper.GetString("key")      // 字符串
viper.GetInt("key")         // 整数
viper.GetBool("key")        // 布尔值
viper.GetFloat64("key")     // 浮点数
viper.GetTime("key")        // 时间
viper.GetDuration("key")    // 时间段
复合类型
viper.GetStringSlice("key")       // 字符串切片
viper.GetIntSlice("key")          // 整数切片
viper.GetStringMap("key")         // 字符串映射
viper.GetStringMapString("key")   // 字符串到字符串的映射
嵌套配置
// 假设配置文件中有 "database.host" 这样的嵌套路径
host := viper.GetString("database.host")
port := viper.GetInt("database.port")// 或者获取整个子树
dbConfig := viper.Sub("database") // 返回一个新的 Viper 实例

3. 默认值

一个好的配置系统应该支持默认值。如果没有通过配置文件、环境变量、远程配置或命令行标志等设置键的值,则默认值非常有用。例如:

viper.SetDefault("port", 3306)  // 如果配置中未设置 "port",则使用默认值
// 或者有
func Viper() {viper.SetDefault("secret_key", "123abc")viper.SetDefault("mysql", map[string]interface{}{"host": "127.0.0.1", "port": 3306})fmt.Println(viper.Get("secret_key"))fmt.Println(viper.Get("MYSQL")) //viper中配置的key值是不区分大小写的
}

4. 环境变量

自动绑定
viper.SetEnvPrefix("APP")      // 环境变量前缀(可选)
viper.AutomaticEnv()           // 自动将环境变量映射到配置(如 APP_PORT → port)// 示例:读取 PORT 环境变量(无前缀)
port := viper.GetInt("port")
手动绑定
viper.BindEnv("port", "APP_PORT")  // 显式绑定环境变量到配置键
Viper 处理 ENV 的核心功能
  1. 读取环境变量
    直接读取系统环境变量,支持通过键名获取值,例如:

    import "github.com/spf13/viper"func main() {// 读取环境变量 "APP_PORT"port := viper.GetInt("app_port") // 自动转换为 int 类型// 或读取原始字符串env := viper.GetString("env")
    }
    
  2. 环境变量前缀(AutoEnv & SetEnvPrefix)
    为避免环境变量键名冲突,Viper 支持设置前缀,自动匹配带前缀的环境变量:

    viper.SetEnvPrefix("myapp") // 设置前缀为 "MYAPP"
    viper.AutomaticEnv() // 启用自动读取环境变量// 此时,Viper 会自动将 "myapp_port" 映射到环境变量 "MYAPP_PORT"
    port := viper.GetInt("port") // 实际读取的是环境变量 "MYAPP_PORT"
    
  3. 环境变量键名映射(BindEnv)
    手动将配置键与环境变量绑定(可自定义环境变量名):

    // 将配置键 "db.host" 与环境变量 "DB_HOST" 绑定
    viper.BindEnv("db.host", "DB_HOST")// 读取时直接用配置键,实际获取的是环境变量 "DB_HOST" 的值
    dbHost := viper.GetString("db.host")
    
  4. 环境变量分隔符(SetEnvKeyReplacer)
    处理配置键中的特殊字符(如 .-)与环境变量命名习惯的冲突:

    import "strings"// 将配置键中的 "." 替换为 "_",以便匹配环境变量
    // 例如 "db.port" 会映射到环境变量 "DB_PORT"
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    viper.AutomaticEnv()dbPort := viper.GetInt("db.port") // 读取环境变量 "DB_PORT"
    
  5. 类型转换
    自动将环境变量的字符串值转换为目标类型(int、bool、float 等),无需手动处理:

    // 环境变量 "MYAPP_DEBUG" 为 "true" 时,会自动转为 bool 类型 true
    debugMode := viper.GetBool("debug")
    
典型使用场景

在实际项目中,通常会结合配置文件(如 YAML/JSON)和环境变量,环境变量可覆盖配置文件中的值,实现灵活的环境适配:

func main() {// 读取配置文件viper.SetConfigFile("config.yaml")viper.ReadInConfig()// 配置环境变量前缀viper.SetEnvPrefix("myapp")viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))viper.AutomaticEnv()// 优先使用环境变量,其次使用配置文件// 例如:环境变量 "MYAPP_DB_PORT" 会覆盖 config.yaml 中的 "db.port"dbPort := viper.GetInt("db.port")
}

5. 命令行参数

结合 pflag
import "github.com/spf13/pflag"func main() {pflag.Int("port", 8080, "服务器端口")pflag.Parse()// 绑定命令行参数到 Viperviper.BindPFlags(pflag.CommandLine)// 使用配置值port := viper.GetInt("port")
}

6. 配置文件监听

viper.WatchConfig()  // 启用配置文件监听// 配置变化时的回调
viper.OnConfigChange(func(e viper.ConfigChangeEvent) {fmt.Println("配置已更新:", e.Name)// 重新加载配置逻辑...
})

7. 远程配置

viper支持监听配置文件,并会在配置文件发生变化,重新读取配置文件和回调函数,这样可以避免每次配置变化时,都需要重启启动应用的麻烦。

  • 功能特点:
    • 集中式配置管理:多个应用实例可从同一个远程配置中心获取最新配置,便于统一管理和维护。
    • 实时配置更新:支持配置热更新,远程配置中心的配置修改后,应用程序能及时获取到最新配置并生效。
    • 更高的安全性:部分场景下支持加密配置,可保护如数据库密码、API 密钥等敏感信息。
    • 本地配置兜底:当远程配置不可用时,可自动回退到本地配置,确保应用程序仍能正常运行。
  • 使用方法:
    • 添加远程提供者:通过viper.AddRemoteProvider("etcd", "http://127.0.0.1:2379", "/config/path")设置远程配置中心的类型、地址和配置路径。这里以 etcd 为例,若使用 Consul 等其他配置中心,只需将类型改为相应的名称,并调整地址等参数即可。
    • 设置配置类型:使用viper.SetConfigType("yaml")指定远程配置的类型,常见的有 yaml、json 等格式,以便 viper 正确解析配置数据。
    • 读取远程配置:调用viper.ReadRemoteConfig()从远程配置中心获取配置信息,若获取失败需处理相应的错误。
    • 绑定配置到变量:可以通过viper.GetString("database.host")等方法将获取到的配置值绑定到程序中的变量,也可通过viper.Unmarshal(&configStruct)将配置解析到自定义的结构体中,方便在代码中使用配置信息。
    • 启用远程监听:使用viper.WatchRemoteConfig()启用远程配置监听功能,通常还会搭配viper.OnRemoteConfigChange(func(){...})注册回调函数,当远程配置发生变化时,回调函数会被触发,可在回调函数中实现更新配置相关的逻辑。
从 Consul/Etcd 读取配置
viper.AddRemoteProvider("consul", "localhost:8500", "config-key")
viper.SetConfigType("yaml")if err := viper.ReadRemoteConfig(); err != nil {// 处理错误
}
监听远程配置变化
// 后台监控进程->配置文件的变化...	viper.WatchConfig()// 回调函数,配置变化时打印发生变更的文件viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config file changed:", e.Name)if err := viper.Unmarshal(Conf); err != nil {fmt.Printf("viper.Unmarshal() failed, err:%v\n", err)//return}})return...
}
// 或者有for {time.Sleep(time.Second * 5)  // 定期检查if err := viper.WatchRemoteConfig(); err == nil {// 配置已更新,重新加载}
}

8. 配置写入

除了读取配置文件外,viper也支持将配置值写入配置文件,viper提供了四个函数,用于将配置写回文件。

  • WriteConfig:将配置写入预先设置好路径的配置文件中,如果配置信息存在,则覆盖,如果没有,则创建。
viper.SetConfigFile("./config/config.yaml")
err := viper.ReadInConfig()
if err != nil {panic(fmt.Errorf("failed to read config file: %s", err))
}
// 设置配置项
viper.Set("mysql.password", "password")
// 写入配置文件
err = viper.WriteConfig()
if err != nil {panic(fmt.Errorf("failed to write config file: %s", err))
}
  • SafeWriterConfig:与WriteConfig函数唯一的不同是如果配置文件存在,则会返回一个错误。
// 设置配置项
viper.Set("mysql.password", "password") //在上面的例子中已经将这个配置写入到了配置文件中
// 写入配置文件
err = viper.SafeWriteConfig()  //返回一个错误
if err != nil {panic(fmt.Errorf("failed to write config file: %s", err))
}
  • WriteConfigAs:与WriteConfig函数的不同是需要传入配置文件保存路径,viper会根据文件后缀判断写入格式。
data := map[string]interface{}{"database": map[string]interface{}{"host":     "localhost","port":     3306,"username": "root","password": "password","dbname":   "demo",},"server": map[string]interface{}{"host": "127.0.0.1","port": 8080,},
}
viper.SetConfigFile("./config/config.yaml")
err := viper.ReadInConfig()
if err != nil {panic(fmt.Errorf("failed to read config file: %s", err))
}//将后端生成的配置数据设置到 Viper 实例中
for key, value := range data {viper.Set(key, value)
}
// 写入配置文件
err = viper.WriteConfigAs("./config/config.toml") //参数是配置文件的路径
if err != nil {panic(fmt.Errorf("failed to write config file: %s", err))
}
  • SafeWriteConfigAs:与WriteConfigAs的唯一不同是如果配置文件存在,则返回一个错误。


http://www.dtcms.com/a/311579.html

相关文章:

  • 牛客 - 旋转数组的最小数字
  • 题单【模拟与高精度】
  • 先学Python还是c++?
  • 工具自动生成Makefile
  • 机器学习——K 折交叉验证(K-Fold Cross Validation),实战案例:寻找逻辑回归最佳惩罚因子C
  • 深入理解C++中的vector容器
  • VS2019安装HoloLens 没有设备选项
  • 大模型(五)MOSS-TTSD学习
  • 二叉树的层次遍历 II
  • 算法: 字符串part02: 151.翻转字符串里的单词 + 右旋字符串 + KMP算法28. 实现 strStr()
  • Redis数据库存储键值对的底层原理
  • 信创应用服务器TongWeb安装教程、前后端分离应用部署全流程
  • Web API安全防护全攻略:防刷、防爬与防泄漏实战方案
  • Dispersive Loss:为生成模型引入表示学习 | 如何分析kaiming新提出的dispersive loss,对扩散模型和aigc会带来什么影响?
  • 二、无摩擦刚体捉取——抗力旋量捉取
  • uniapp 数组的用法
  • 【c#窗体荔枝计算乘法,两数相乘】2022-10-6
  • Python Pandas.from_dummies函数解析与实战教程
  • 【语音技术】什么是动态实体
  • 【解决错误】IDEA启动SpringBoot项目 出现:Command line is too long
  • 5734 孤星
  • process_vm_readv/process_vm_writev 接口详解
  • 如何在 Ubuntu 24.04 或 22.04 LTS Linux 上安装 Guake 终端应用程序
  • Next.js 怎么使用 Chakra UI
  • LINUX82 shell脚本变量分类;系统变量;变量赋值;四则运算;shell
  • 落霞归雁·思维框架
  • 队列的使用【C++】
  • 【王阳明代数讲义】基本名词解释
  • InfluxDB 与 Node.js 框架:Express 集成方案(一)
  • 【RK3568 RTC 驱动开发详解】