go语言后端开发学习(五)——如何在项目中使用Viper来配置环境

ops/2024/9/20 7:22:50/ 标签: golang, 学习, 开发语言

前言

在之前的文章中我们就介绍过用go-ini来读取配置文件,但是当时我们在介绍时说了他只能读取.ini格式的配置文件所以局限性较大,这里我们介绍一个适用范围更大的配置管理第三方库——Viper

什么是Viper

Viper是适用于Go应用程序(包括Twelve-Factor App)的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:

  • 设置默认值
  • 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或Consul)读取并监控配置变化
  • 从命令行参数读取配置
  • 从buffer读取配置
  • 显式配置值

Viper的安装

和安装其他第三方库没什么区别,执行下面这一命令即可

go get github.com/spf13/viper

把值存入Viper

1.给读取的变量设置默认值

在我们读取配置文件时,为了防止读取配置是出现不必要的错误所以给键设置默认值是十分有必要的事,而Viper中我们也可以设置默认值,比如下面这样设置:

     viper.SetDefault("AppMode","debug")viper.SetDefault("AppPort", "8080")

2.读取配置文件

在读取配置文件过程中,Viper需要最少知道在哪里查找配置文件的配置。Viper支持JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件。Viper可以搜索多个路径,但目前单个Viper实例只支持单个配置文件。Viper不默认任何配置搜索路径,将默认决策留给应用程序。

我们在使用Viper搜索和读取配置文件,不需要任何特定的路径,但是要提供一个配置文件预期出现的路径,比如下面这样:

viper.SetConfigFile("./config.yaml") // 指定配置文件路径
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
viper.AddConfigPath("/etc/appname/")   // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname")  // 多次调用以添加多个搜索路径
viper.AddConfigPath(".")               // 还可以在工作目录中查找配置
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

示例:
我这里创建了一个config.ini,内容如下:

[server]
AppMode=debug
HttpPort=:3000
JWTKey=FenXu123

我们可以尝试用Viper来读取一下配置文件:

package mainimport ("fmt""github.com/spf13/viper"
)func main() {viper.SetConfigName("config")viper.AddConfigPath("./src/demo/conf")err := viper.ReadInConfig() // 查找并读取配置文件if err != nil {             // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))}appmode := viper.GetString("server.AppMode")print(appmode)
}

这样我们就可以获取到配置文件里面的配置了。

3.写入配置文件

我们可以在配置文件中读取配置文件,但是有时候我们也会需要存储在运行时对配置文件所做的修改,这就需要我们将变化写入到配置文件中,而针对这种情况我们可以使用一以下的几个函数:

viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过,所以会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")

我们来看一下这几个函数:

  • WriteConfig 将当前的viper配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
  • SafeWriteConfig 将当前的viper配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。
  • WriteConfigAs 将当前的viper配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
  • SafeWriteConfigAs 将当前的viper配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。

总结一下,标记为safe的函数不会覆盖原有的配置文件,而是之间创建

4.监控并重新读取配置文件

相对于go-ini每次需要停止程序的运行再实时读取配置文件,Viper支持我们在运行时读取配置文件的更新,我们可以通过下面的代码尝试一下:

package mainimport ("fmt""github.com/fsnotify/fsnotify""github.com/spf13/viper""time"
)func main() {viper.SetConfigName("config")viper.AddConfigPath("./src/demo/conf")err := viper.ReadInConfig() // 查找并读取配置文件if err != nil {             // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))}PrintConfig()       //打印当前配置viper.WatchConfig() // 监控配置文件变化并热加载程序viper.OnConfigChange(func(e fsnotify.Event) {fmt.Println("Config file changed:", e.Name)PrintConfig()})for {print("1111\n")time.Sleep(time.Second * 100)}
}func PrintConfig() {fmt.Println("conf.AppMode: ", viper.GetString("server.AppMode"))fmt.Println("conf.HttpPort:", viper.GetString("server.HttpPort"))fmt.Println("conf.JWTKey:", viper.GetString("server.JWTKey"))
}

5.从io.Reader中读取配置

除了从配置源(比如环境变量/配置文件等地方)来获取配置文件信息,我们还可以自己定义配置文件信息比如这样:

package mainimport ("bytes""fmt""github.com/spf13/viper"
)var config = []byte(`
AppMode: debug
HttpPort: 8080
JWTKey: 123456
`)func main() {viper.SetConfigType("yaml")  //这里要说明io.Reader中我们的书写格式err := viper.ReadConfig(bytes.NewBuffer(config)) // 查找并读取配置文件if err != nil {                                  // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))}PrintConfig() //打印当前配置
}func PrintConfig() {fmt.Println("AppMode: ", viper.Get("AppMode"))fmt.Println("HttpPort:", viper.Get("HttpPort"))fmt.Println("JWTKey:", viper.Get("JWTKey"))
}

当然我们也可以手动设置值

viper.Set("AppMode", "release") 

从Viper中获取值

1.常用的方法

在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:

  • Get(key string) : interface{}
  • GetBool(key string) : bool
  • GetFloat64(key string) : float64
  • GetInt(key string) : int
  • GetIntSlice(key string) : []int
  • GetString(key string) : string
  • GetStringMap(key string) : map[string]interface{}
  • GetStringMapString(key string) : map[string]string
  • GetStringSlice(key string) : []string
  • GetTime(key string) : time.Time
  • GetDuration(key string) : time.Duration
  • IsSet(key string) : bool
  • AllSettings() : map[string]interface{}

Get为前缀的方法的作用非常好理解,它的作用主要是将获取到的键值转换为对的形式,而IsSet检查指定键是否已经被设置。如果键存在于配置中,返回 true;否则返回 false

2.获取嵌套的键

如果现在有深度嵌套键的格式化路径,比如下面这种Json文件需要我们去读取:

{"host": {"address": "localhost","port": 5799},"datastore": {"metric": {"host": "127.0.0.1","port": 3099},"warehouse": {"host": "198.0.0.1","port": 2112}}
}

我们尝试读取一下127.0.0.1的的配置:

viper.GetString("datastore.metric.host")

3.提取子树

假设我们现在有多个组件的配置需要加载,比如这样:

app:cache1:max-items: 100item-size: 64cache2:max-items: 200item-size: 80

我们可以将cache1cache2分别映射到两个实例中可以这么写:

cfg1 := viper.Sub("app.cache1")  //提取信息
cache1 := NewCache(cfg1) //初始化实例cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)

大家可能好奇这样有什么好处,主要是通过这种方式,我们可以轻松地处理多个缓存配置,每个缓存都有自己的独立配置,而不会相互干扰。这在构建复杂的应用程序时特别有用,其中不同的组件或服务可能需要不同的配置参数。

4.反序列化

我们还可以尝试将所有或特定的值解析到结构体中,这里我们主要会用到下面两个函数:

Unmarshal(rawVal interface{}) error  //将 viper 实例中的所有配置数据解码到给定的结构体中
UnmarshalKey(key string, rawVal interface{}) error // 将 viper 实例中指定键的配置数据解码到给定的结构体中

这里也可以看看下面的两个简单示例:

type Config struct {Server struct {Port int    `mapstructure:"port"`Host string `mapstructure:"host"`} `mapstructure:"server"`Database struct {User     string `mapstructure:"user"`Password string `mapstructure:"password"`Name     string `mapstructure:"name"`} `mapstructure:"database"`
}var cfg Config
err := viper.Unmarshal(&cfg)
if err != nil {fmt.Println("Error unmarshalling config:", err)
}
type CacheConfig struct {MaxSize int    `mapstructure:"max_size"`Timeout string `mapstructure:"timeout"`
}var cacheCfg CacheConfig
err := viper.UnmarshalKey("app.cache1", &cacheCfg)
if err != nil {fmt.Println("Error unmarshalling cache config:", err)
}

5.序列化

我们还可以将Viper的配置全部序列到一个字符串中,同时我们还可以将这个配置用自己喜欢的格式进行序列化来使用,代码如下:

func main() {viper.SetConfigType("yaml")                      //这里要说明io.Reader中我们的书写格式err := viper.ReadConfig(bytes.NewBuffer(config)) // 查找并读取配置文件if err != nil {                                  // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))}c := viper.AllSettings()fmt.Println(c) // 打印配置文件bs, err := json.Marshal(c)  // 将配置文件序列化成jsonfmt.Println(string(bs))
}

运行结果如下:
在这里插入图片描述
最后我们来实现一个简单的viper使用样例,大家在以后项目可以做到开盒即用:

package mainimport ("fmt""github.com/fsnotify/fsnotify""github.com/spf13/viper"
)type Server struct {HttpPort stringAppMode  stringJwtKey   string
}func main() {viper.AddConfigPath("./src/demo/conf")viper.SetConfigName("config")viper.SetConfigType("ini")if err := viper.ReadInConfig(); err != nil {panic(err)}PrintConfig()if err := viper.Unmarshal(&Server{}); err != nil {panic(err)}viper.WatchConfig() //监听配置文件变化并热加载程序viper.OnConfigChange(func(in fsnotify.Event) {fmt.Println("配置文件修改了")if err := viper.Unmarshal(&Server{}); err != nil {panic(err)}})
}func PrintConfig() {fmt.Println("HttpPort:", viper.GetString("server.HttpPort"))fmt.Println("AppMode:", viper.GetString("server.AppMode"))fmt.Println("JwtKey:", viper.GetString("server.JWTKey"))
}

拓展:如何优雅地关机或重启

1.什么是优雅关机及其实现

优雅关机就是服务端关机命令发出后不是立即关机,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的关机方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。

接下来我们可以看一下如何实现一个简单的优雅关机:

package mainimport ("context""fmt""github.com/gin-gonic/gin""net/http""os""os/signal""syscall""time"
)func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {time.Sleep(5 * time.Second)c.JSON(200, gin.H{"message": "pong",})})srv := &http.Server{Addr:    ":8080",Handler: r,}go func() { //启动http服务if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {panic(err)}}()quit := make(chan os.Signal, 1) //协程一协程之间用管道通讯//signal.Notify 用于将指定的系统信号发送到一个 channel。这样你可以在你的程序中监听这些信号并做出相应的处理signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) //此时监听到信号,quit接收信号,如果没有接收到信号程序阻塞<-quit//优雅关闭ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {panic(err)}fmt.Println("Server exiting")
}

优雅重启:

package mainimport ("log""net/http""time""github.com/fvbock/endless""github.com/gin-gonic/gin"
)func main() {router := gin.Default()router.GET("/", func(c *gin.Context) {time.Sleep(5 * time.Second)c.String(http.StatusOK, "hello gin!")})// 默认endless服务器会监听下列信号:// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP// 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)// 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机// 接收到 SIGUSR2 信号将触发HammerTime// SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数if err := endless.ListenAndServe(":8080", router); err!=nil{log.Fatalf("listen: %s\n", err)}log.Println("Server exiting")
}

http://www.ppmy.cn/ops/93845.html

相关文章

Ubuntu系统的基础操作和使用|Linux|安装|网络连接|更新与升级系统|系统维护|故障排除|监控|桌面环境|虚拟机|快捷键

目录 1. Ubuntu系统的安装与初步设置 1.1 下载与安装Ubuntu 1.2 创建用户和设置密码 1.3 配置网络连接 1.4 更新与升级系统 2. Ubuntu的基本操作 2.1 文件与目录管理 2.2 系统进程管理 2.3 软件安装与管理 2.4 权限与用户管理 3. 系统维护与故障排除 3.1 系统日志查…

HarmonyOS鸿蒙开发岗位面试中关于组件的问题总结

文章目录 1. 鸿蒙组件的基本概念2. 组件的使用3. 布局管理4. 组件间通信5. 组件化开发6. 性能优化7. 实战应用 鸿蒙应用开发岗位面试中关于鸿蒙组件的问题&#xff0c;通常会涉及多个关键知识点&#xff0c;这些知识点涵盖了鸿蒙组件的基本概念、使用、布局管理、性能优化、组件…

Go语言并发编程实战:掌握并发模型,提升应用性能

1. 引言 1.1 并发编程的重要性 在现代软件开发中&#xff0c;并发编程已经成为了一种不可或缺的技术。随着多核处理器的普及和云计算的兴起&#xff0c;应用程序需要能够有效地利用并发处理能力&#xff0c;以提高性能和用户体验。并发编程使得程序能够在同一时间内处理多个任…

Qt Xlsx使用教程、Qt操作Excel、Qt生成Excel图表、跨平台不依赖Office 直接使用源码

1.Qt Xlsx库简介 官方文档&#xff1a;Qt Xlsx | QtXlsx 0.3 (debao.me) 下载地址&#xff1a;dbzhang800/QtXlsxWriter: .xlsx file reader and writer for Qt5 (github.com) CSDN下载地址&#xff1a;QtXlsxWriter-master源码资源-CSDN文库 2.源码取出 3.目录结构 再根目…

股指期货套期保值中的展期管理有哪些?

在复杂的金融市场环境中&#xff0c;展期作为一种重要的风险管理工具&#xff0c;被广泛应用于期货交易中&#xff0c;特别是当投资者需要对长期资产进行套期保值时。展期的核心思想在于&#xff0c;通过连续替换高流动性的近月期货合约来替代流动性较差的远月合约&#xff0c;…

JS【详解】对象的内部属性 vs 内部方法

每个JS 对象都有很多内部属性和方法&#xff0c;仅供 JS 引擎管理和操作对象使用&#xff0c;对开发者不可见&#xff0c;只能用特殊的方法访问和修改&#xff08;不建议修改&#xff09; 了解它们可以帮助我们更好的理解对象的行为&#xff0c;无需深究其具体实现 下文中&am…

力扣:496. 下一个更大元素 I、503. 下一个更大元素 II

496. 下一个更大元素 I 这里我们采用单调栈来写这道题。 首先遍历nums2,并新开一个数组ant&#xff0c;存储对应nums2的下一个更大元素&#xff0c;这里采用单调栈&#xff0c;从栈顶到栈底是递增序列。 然后我们遍历a,再遍历b找到对应nums1nums2&#xff0c;然后nums1存储a…

*算法训练(leetcode)第四十五天 | 101. 孤岛的总面积、102. 沉没孤岛、103. 水流问题、104. 建造最大岛屿

刷题记录 101. 孤岛的总面积DFSBFS 102. 沉没孤岛DFSBFS *103. 水流问题*104. 建造最大岛屿 101. 孤岛的总面积 题目地址 本题要求不与矩阵边缘相连的孤岛的总面积。先将与四个边缘相连的岛屿变为海洋&#xff0c;再统计剩余的孤岛的总面积。无需再标识访问过的结点&#xff…

利用Python轻松从视频中抽取帧

利用Python轻松从视频中抽取帧 安装依赖示例代码参数说明使用示例 在做CV项目的时候&#xff0c;有时候可能需要从视频中抽取一些有价值的图片&#xff0c;可以使用 Python 的 opencv 库来从视频中抽取帧。以下是一个示例程序&#xff0c;展示了如何从视频中抽取帧&#xff0c;…

单调栈(算法篇)

算法之单调栈 注意&#xff1a;单调栈是一种数据结构&#xff0c;并非是一种算法&#xff0c;但是我们做一些算法题的时候&#xff0c;这种单调性结构有妙用&#xff0c;所以我姑且放在算法篇进行讲解 单调栈 概念&#xff1a; 单调栈是一种数据结构&#xff0c;但是因为经…

面试实战题-数据结构与算法

数据结构与算法 求TopK 大根堆 解题思路&#xff1a;保持堆的大小为K&#xff0c;然后遍历数组中的数字&#xff0c;遍历的时候做如下判断&#xff1a; * 1. 若目前堆的大小小于K&#xff0c;将当前数字放入堆中。 * 2. 否则判断当前数字与大根堆堆顶元素的大小关系&#xf…

Unity动画模块 之 2D IK(反向动力学)

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正​ 1.什么是IK 反向动力学 IK&#xff08;Inverse Kinematics&#xff09;是一种方法&#xff0c;可以根据某些子关节的最…

[upload]-[GXYCTF2019]BabyUpload1-笔记

尝试上传.htaccess和图片和一句话木马提示 php文件提示 响应头可以看到 构造一句话图片木马如下&#xff1a; <script languagephp>eval($_POST[cmd]);</script> 上传成功 必须增加文件夹下jpg后缀解析php .htaccess如下 <FilesMatch "jpg">Set…

「11月·香港」第三届人工智能、人机交互和机器人国际学术会议(AIHCIR 2024)

第三届人工智能、人机交互和机器人国际学术会议&#xff08;AIHCIR 2024&#xff09;组委会热忱地邀请您参与本届大会。本届大会旨在聚集领先的科学家、研究人员和学者&#xff0c;共同交流和分享在人工智能、人机交互和机器人各个方面的经验和研究成果&#xff0c;为研究人员、…

Godot《躲避小兵》实战之设置项目

通过之前的学习我们已经基本了解了godot的界面&#xff0c;知道如何创建项目以及节点。那么&#xff0c;从这一章节我们将进入godot官方给我们提供的一个2D游戏开发的小教程进行入手&#xff0c;这个游戏并不是我自己的作品&#xff0c;而是我通过学习完之后&#xff0c;对其进…

玩转生产环境全链路压测

一、什么是生产环境全链路压测 生产环境全链路压力测试&#xff08;Production Environment Full-Link Stress Testing&#xff09;是一种针对线上系统进行的综合性性能测试方法。这个过程涉及模拟实际用户行为&#xff0c;从用户界面到后端数据库的整个应用链路上施加预定的高…

Python基础教程:正则表达式

Python基础教程&#xff1a;正则表达式 概述 正则表达式&#xff08;Regular Expression&#xff0c;简称Regex&#xff09;是一种用于匹配字符串中字符组合的模式。Python的re模块提供了广泛的正则表达式功能&#xff0c;可以用来执行各种字符串搜索、替换和分割操作。 1. …

联通数科如何基于Apache DolphinScheduler构建DataOps一体化能力平台

各位小伙伴晚上好&#xff0c;我是联通数字科技有限公司数据智能事业部的王兴杰。 更好的阅读体验可前往原文阅读:巨人肩膀 | 联通数科如何基于Apache DolphinScheduler构建DataOps一体化能力平台 今天&#xff0c;我将和大家聊一聊联通数字科技有限公司是如何基于Apache Dol…

设计模式-单例设计模式

单例模式的设计和线程安全 单例模式是一种创建型设计模式&#xff0c;确保一个类只有一个实例&#xff0c;并提供一个全局访问点。实现单例模式时&#xff0c;线程安全性是一个重要考虑因素&#xff0c;特别是在多线程环境中。 1. C11 之前的线程安全实现 在 C11 之前&#…

NAT模式搭建实战

一、NAT模式搭建实战 1.给nat机新添加一块网卡 [rootnat ~]# vim /etc/sysconfig/network-scripts/ifcfg-ens36 TYPE"Ethernet" BOOTPROTO"none" DEVICE"ens36" NAME"ens36" UUID"d0f9b80a-e098-3e1f-9ec3-0a502b1ed00e&q…