前言
在之前的文章中我们就介绍过用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)
: boolGetFloat64(key string)
: float64GetInt(key string)
: intGetIntSlice(key string)
: []intGetString(key string)
: stringGetStringMap(key string)
: map[string]interface{}GetStringMapString(key string)
: map[string]stringGetStringSlice(key string)
: []stringGetTime(key string)
: time.TimeGetDuration(key string)
: time.DurationIsSet(key string)
: boolAllSettings()
: 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
我们可以将cache1
和cache2
分别映射到两个实例中可以这么写:
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")
}