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

简介: Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML

前言

在之前的文章中我们就介绍过用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")
}
相关文章
|
9天前
|
程序员 Go PHP
为什么大部分的 PHP 程序员转不了 Go 语言?
【9月更文挑战第8天】大部分 PHP 程序员难以转向 Go 语言,主要因为:一、编程习惯与思维方式差异,如语法风格和编程范式;二、学习成本高,需掌握新知识体系且面临项目压力;三、职业发展考量,现有技能价值及市场需求不确定性。学习新语言虽有挑战,但对拓宽职业道路至关重要。
39 10
|
7天前
|
Go API 开发者
深入探讨:使用Go语言构建高性能RESTful API服务
在本文中,我们将探索Go语言在构建高效、可靠的RESTful API服务中的独特优势。通过实际案例分析,我们将展示Go如何通过其并发模型、简洁的语法和内置的http包,成为现代后端服务开发的有力工具。
|
9天前
|
编译器 Go
go语言学习记录(关于一些奇怪的疑问)有别于其他编程语言
本文探讨了Go语言中的常量概念,特别是特殊常量iota的使用方法及其自动递增特性。同时,文中还提到了在声明常量时,后续常量可沿用前一个值的特点,以及在遍历map时可能遇到的非顺序打印问题。
|
6天前
|
存储 监控 数据可视化
Go 语言打造公司监控电脑的思路
在现代企业管理中,监控公司电脑系统对保障信息安全和提升工作效率至关重要。Go 语言凭借其高效性和简洁性,成为构建监控系统的理想选择。本文介绍了使用 Go 语言监控系统资源(如 CPU、内存)和网络活动的方法,并探讨了整合监控数据、设置告警机制及构建可视化界面的策略,以满足企业需求。
24 1
|
4天前
|
设计模式 安全 数据库连接
后端开发中的设计模式应用
在软件开发的浩瀚海洋中,设计模式如同灯塔,为后端开发者指引方向。它们不仅仅是代码的模板,更是解决复杂问题的智慧结晶。本文将深入探讨几种常见的设计模式,包括单例模式、工厂模式和观察者模式,并揭示它们在实际应用中如何提升代码的可维护性、扩展性和重用性。通过实例分析,我们将一窥这些模式如何在后端开发中大放异彩,助力构建高效、灵活的软件系统。
|
3天前
|
存储 前端开发 关系型数据库
探索后端开发:从新手到专家的旅程
【9月更文挑战第15天】在数字时代的浪潮中,后端开发成为了技术世界的心脏。本文将通过浅显易懂的语言和实际案例,带领读者从零开始,逐步深入后端开发的奥秘。我们将一探究竟,如何从基础概念出发,通过实践操作,最终成长为能够独立设计和实现复杂系统的后端专家。无论你是编程新手还是有一定经验的开发者,这篇文章都将为你的职业成长之路提供宝贵的指导和灵感。
31 15
|
8天前
|
存储 JavaScript Java
深入浅出后端开发:从零到一的旅程
【9月更文挑战第10天】本文将带领读者走进后端开发的神秘世界,从基本概念到实战应用,一步步揭开后端技术的面纱。通过浅显易懂的语言和实际代码示例,我们将探索如何搭建一个简单的后端服务,理解数据库交互,并实现API的基本功能。无论你是编程新手还是希望扩展知识的开发者,这篇文章都将为你提供一条清晰的学习路径。
|
7天前
|
前端开发 JavaScript Java
编程入门之前端和后端开发
前端开发就是开发网页上的内容展示与用户的交互,一部分后端开发工作就是开发数据访问服务,使前端可以通过后端服务对数据进行增删改查,也就是Crud,对前端用户的请求进行相应。
27 8
|
4天前
|
Web App开发 存储 JavaScript
深入浅出Node.js后端开发
【9月更文挑战第15天】在数字化浪潮中,Node.js作为一颗耀眼的星辰,为后端开发领域注入了活力与创新。本文将带你领略Node.js的魅力所在,探索其架构设计、性能优化及实战应用,让你在轻松愉快的氛围中掌握Node.js后端开发的精髓。
|
8天前
|
存储 前端开发 Java
探索后端开发:从新手到专家的旅程
在这篇文章中,我们将一起踏上一段奇妙的旅程,探索后端开发的奥秘。无论你是刚刚踏入编程世界的新手,还是已经在这条路上走了一段时间的开发者,这篇文章都将为你提供有价值的见解和建议。我们将从基础概念开始,逐步深入到高级主题,包括框架选择、数据库设计、API开发等。让我们一起开启这段旅程吧!

热门文章

最新文章