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

news2024/9/23 7:26:07

前言

在之前的文章中我们就介绍过用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 main

import (
	"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 main

import (
	"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 main

import (
	"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: 100
    item-size: 64
  cache2:
    max-items: 200
    item-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)  // 将配置文件序列化成json
	fmt.Println(string(bs))
}

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

package main

import (
	"fmt"
	"github.com/fsnotify/fsnotify"
	"github.com/spf13/viper"
)

type Server struct {
	HttpPort string
	AppMode  string
	JwtKey   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 main

import (
	"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 main

import (
	"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.coloradmin.cn/o/2036779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法-差分, 二维前缀和, 离散化

文章目录 本节提要1. 一维差分1.1 一维差分原理分析1.2 一维差分例题应用 2. 等差数列差分2.1 等差数列差分原理分析 3. 二维前缀和3.1 二维前缀和原理分析3.2 二维前缀和例题应用 本节提要 本节的主要目标是一维差分的总结, 包括一维差分, 一维等差数列差分; 二维差分和二维前…

唐诡探案外传-MySQL误操作之“寻凶”

序幕&#xff1a;字符集被修改之谜 立秋后的第一天下午&#xff0c;太阳仍不知疲倦地炙烤着大地&#xff0c;办公室内的中央空调不停歇地鼓吹着冷风。某办公楼内&#xff0c;IT部门的阿祖被同事急匆匆地找来&#xff0c;说是系统出现了奇怪的bug&#xff0c;追查之下发现测试环…

搭建高可用OpenStack(Queen版)集群(十二)之启动一个实例

一、搭建高可用OpenStack&#xff08;Queen版&#xff09;集群之启动一个实例 创建实例之前&#xff0c;首先要创建虚拟网络 一、创建网络 建议命令行和web管理协同进行配置&#xff0c;这样不容易出错 在控制节点执行 1、创建公有网络部分 1、在命令行执行 . admin-openrc ne…

【SQL】平均售价

目录 题目 分析 代码 题目 表&#xff1a;Prices ------------------------ | Column Name | Type | ------------------------ | product_id | int | | start_date | date | | end_date | date | | price | int | ---------------…

技术研究:Redis 数据结构与 I/O 模型

数据结构 Redis之所以“快”&#xff0c;一方面因为它是内存数据库&#xff0c;所有操作都在内存上完成&#xff0c;内存的访问速度本来就快。另一方面则是因为高效的数据结构&#xff0c;使得操作键值效率较高。总体来说&#xff0c;Redis使用了一个用来保存每个Key/Value的全…

【Story】如何高效记录并整理编程学习笔记?

目录 一、为何笔记在编程学习中如此重要&#xff1f;1.1 知识的捕捉1.2 理解和消化1.3 知识的复习1.4 知识的分享 二、建立高效的笔记系统2.1 确定笔记的目标2.2 选择合适的工具2.3 笔记的结构化2.4 记录有效的内容2.5 定期回顾和更新 三、保持笔记条理性的技巧3.1 使用一致的格…

【数据结构】堆排序与TOP-K问题

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 文章目录 1.堆排序1.1 建堆1.2 利用堆删除思想来进行排序1.3 堆排序的时间复杂度 2.TOP-K问…

Uniapp之微信小程序计算器

UI仿的iOS手机计算器&#xff0c;基本功能已经实现&#xff0c;如下效果图 具体使用可以参考微信小程序&#xff1a;日常记一记--我的---计算器 第一步&#xff1a;UI界面设计 1&#xff0c;strClass模块是计算过程代码展示 2&#xff0c;result-view模块是结果展示 3&#xff…

嵌入式学习---DAY24:进程--二

一、exec函数族----启动一个新程序 用fork创建子进程后执行的是和父进程相同的程序&#xff08;但有可能执行不同的代码分支&#xff09;&#xff0c; 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时&#xff0c;该进程的 用户空间代码和数据完全被…

SHT30温湿度传感器全解析——概况,性能,MCU连接,样例代码

常见温湿度传感器测量范围&#xff1a;(价格仅供参考&#xff0c;具体性能要看折线图) 型号DHT11DHT20AHT10AHT20AHT30SHT20价格&#xffe5; 2.49&#xffe5;3.04&#xffe5; 1.9&#xffe5;1.4&#xffe5; 1.3&#xffe5;5.5温度测量范围20—90%RH0—100%RH0—100%RH0—…

pycharm最新专业版激活码

pycharm最新专业版激活码 Pycharm下载地址&#xff1a;pycharm下载 首先&#xff0c;我们打开下载的 pycharm 专业版并安装。 按照下图所示先点击上方的 Activation code&#xff0c;再将激活码粘贴至输入框&#xff0c;最后点击 Activate 激活。 激活码&#xff1a; OS2AN…

旋转关系介绍

目录 旋转矩阵与轴角 旋转矩阵与欧拉角 旋转矩阵与四元数 轴角与四元数 轴角与欧拉角 欧拉角与四元数 欧拉角与四元数 旋转矩阵与轴角 设旋转矩阵R[■8(r_11&r_12&r_13r_21&r_22&r_23r_31&r_32&r_33)]&#xff0c;轴角使用一个单位向量n和一个角…

Go--GMP调度模型

目录 GMP模型G、M、P简介P和M的个数**P和M何时会被创建**goroutine创建流程goroutine什么时候会被挂起 GMP的调度调度流程调度策略调度时机同时启动了一万个goroutine&#xff0c;会如何调度&#xff1f; GMP模型 G、M、P简介 GMP是Go运行时调度层面的实现&#xff0c;包含4个…

质量对中国开发商提升游戏品牌信誉和信任度的影响

随着全球游戏产业的持续增长&#xff0c;中国开发商正在大举进军国际市场。然而&#xff0c;他们面临的关键挑战之一是建立和维护与全球参与者的品牌信誉和信任。他们的游戏质量在实现这一目标方面起着至关重要的作用。从技术性能到故事讲述和本地化&#xff0c;高质量的游戏对…

OpenGL3.3_C++_Windows(35)

PBR_IBL漫反射 IBL图像的光照(Image based lighting&#xff09;&#xff1a;非直接光源&#xff0c;它是一种更精确的环境光照输入格式&#xff0c;甚至也可以说是一种全局光照的粗略近似。环境光照&#xff1a;获取每个wi光源辐射率&#xff0c;求辐照度&#xff1a;将周围环…

Linux学习笔记11(计算机网络)

目录 网络七层模型/五层模型 IP地址分类 CIDR Centos7的网卡IP配置 RockyLinux9的网卡IP配置 网络七层模型/五层模型 自下到上 物理层&#xff1a; 建立物理连接&#xff0c;传输 0 和 1 的比特流 数据链路层&#xff1a; 物理地址寻址&#xff0c;流量控制&#xff0c;差错…

基于vue框架的SSM基于B_S的毕业设计题目管理系统的设计与实现ij0q7(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,教师,毕设题目,毕设选题,毕设任务书,开题报告,中期检查,毕业论文,论文成绩,答辩成绩,答辩通知,班级 开题报告内容 基于Vue框架的SSM&#xff08;SpringSpring MVCMyBatis&#xff09;的毕业设计题目管理系统设计与实现 开题报告…

刷题记录第109天-K个一组反转链表

解题思路&#xff1a; 第一步&#xff1a;实现一个数组&#xff0c;给定一段链表的头结点和尾节点&#xff0c;反转该链表&#xff0c;并返回新的头结点和尾结点。 第二步&#xff1a;初始化一个虚拟头结点&#xff0c;用于记录最终头结点和规范操作。 第三步&#xff1a;给定一…

​产品经理-​你如何理解“互联网思维(35)

在产品规划和功能改版中&#xff0c;确实非常重视用户需求和体验。产品需求是互联网产品的核心 用户体验是互联网产品的重点。在互联网新产品规划中&#xff0c;会非常重视用户验证环节 确保做出来的东西确实是用户想要的&#xff1b;而在已经上线的产品中&#xff0c;往往会有…

Raspberry Pi Pico 家族的进化 —— RP2040、RP2350与RP2354性能比较

随着树莓派Pico系列的不断扩展&#xff0c;其背后的芯片也得到了升级和改进。从最初的RP2040到最新的RP2354&#xff0c;每一次迭代都带来了新的功能和性能提升。本文将详细对比RP2040、RP2350和RP2354三款芯片的关键特性&#xff0c;帮助开发者了解它们的差异&#xff0c;并选…