Skip to main content

Viper

Viper

Viper是一个为Go应用程序(包括12-Factor应用)提供的完整配置解决方案。它旨在在应用程序中工作,能够处理所有类型的配置需求和格式。

Viper 支持以下功能:

  • 设置默认值。
  • 从JSON、TOML、YAML、HCL、envfile和Java Properties配置文件中读取配置。
  • 实时监控并重新读取配置文件(可选)。
  • 从环境变量中读取配置。
  • 从远程配置系统(如etcd或Consul)中读取配置,并监控变更。
  • 从命令行标志中读取配置。
  • 从缓冲区中读取配置。
  • 设置显式值。
go get github.com/spf13/viper

在构建现代应用程序时,你不希望为配置文件格式烦恼,而是想专注于构建出色的软件。Viper 可以帮助你实现这一目标。

Viper做了以下工作:

  • 查找、加载并解组(unmarshal)JSON、TOML、YAML、HCL、INI、envfile或Java Properties格式的配置文件。
  • 提供设置不同配置选项默认值的机制。
  • 提供通过命令行标志指定选项的覆盖值设置机制。
  • 提供一个别名系统,以便轻松重命名参数而不会破坏现有代码。
  • 使你能够轻松区分用户提供的命令行或配置文件与默认值是否相同。

Viper 使用以下优先级顺序,每项的优先级高于其下方的项:

  1. 显式调用Set
  2. 命令行标志。
  3. 环境变量。
  4. 配置文件。
  5. 键/值存储。
  6. 默认值。

配置与读取

Set/Get

可使用SetGet方法进行配置的设置和读取:

package main

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

func main() {
viper.Set("APP_ENV", "development")
mode := viper.Get("APP_ENV")
fmt.Println(mode)
}

SetConfigFile

SetConfigFile可用来设置一个具体的配置文件:

viper.SetConfigFile("./app.env")

SetConfigFile目前只能设置一个配置文件,如果设置多次则以最后一次设置为准。

ReadInConfig

ReadInConfig方法可读取配置:

package main

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

func main() {
viper.SetConfigFile("./app.env")

err := viper.ReadInConfig()

if err != nil {
log.Fatalf("Failed to read config file: %s", err.Error())
}

mode := viper.Get("APP_ENV")

fmt.Println(mode)
}

SetConfig

  • AddConfigPath 设置配置文件路径。
  • SetConfigName 设置配置文件名称。
  • SetConfigType 设置配置文件类型。

这三个方法的组合使用可看作SetConfigFile的拆解版本:

package main

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

func main() {
viper.AddConfigPath("./")
viper.SetConfigName("app")
viper.SetConfigType("yaml")

err := viper.ReadInConfig()

if err != nil {
log.Fatalf("Failed to read config file: %s", err.Error())
}

mode := viper.Get("APP_ENV")

fmt.Println(mode)
}

在进行配置文件搜索时,会以配置文件名称为高优先级进行搜索:

  • 如果未查找到对应的文件名称,则会报错。
  • 如果查找到对应的文件名称,但类型不匹配则以查找到的为准。
  • 如果类型不是Viper支持的类型,则也会报错。

别名

可以给配置字段通过RegisterAlias设置别名:

package main

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

func main() {
viper.Set("APP_ENV", "development")
viper.RegisterAlias("env", "APP_ENV")
mode := viper.Get("env")
fmt.Println(mode)
}

默认值

可通过SetDefault来设置默认值:

package main

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

func main() {
viper.SetDefault("APP_ENV", "development")
viper.SetDefault("db", map[string]any{
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"password": "12345678",
"dbName": "golang",
})

dbInfo := viper.Get("DB")
fmt.Println(dbInfo)
}

Viper中配置的key的大小写是不敏感的,因此dbDB本质上是设置的同一个key值。

写入文件

可通过WriteConfigWriteConfigAsSafeWriteConfigSafeWriteConfigAs等方法写入配置文件,适合保存、迁移配置文件:

package main

import (
"log"
"github.com/spf13/viper"
)

func main() {
viper.SetConfigFile("./app.yaml")

viper.SetDefault("APP_ENV", "development")
viper.Set("db", map[string]any{
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"password": "12345678",
"dbName": "golang",
})
viper.Set("db.type", "mysql")

err := viper.WriteConfig()
// err := viper.WriteConfigAs("./app.json")

if err != nil {
log.Fatalf("Failed to write config file: %s", err.Error())
return
}
}

监听变化

可通过WatchConfig来监听配置文件的变化:

package main

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

func main() {
viper.SetDefault("APP_ENV", "development")

viper.SetConfigFile("./app.yaml")

err := viper.ReadInConfig()

if err != nil {
log.Fatalf("Failed to read config file: %s", err.Error())
return
}

viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println(in.Name)
})

if err := viper.WriteConfig(); err != nil {
log.Fatalf("Failed to write config file: %s", err.Error())
return
}
}

通常通过此种方式将配置解析为结构体。

项目应用

gin:
ip: 0.0.0.0
port: 3300

db:
host: 127.0.0.1
port: 3306
user: root
password: 12345678
db_name: golang

环境变量

Viper可以访问系统环境变量,可以根据环境变量加载不同的应用环境。

Mac下设置环境变量:

打开用户目录下.bash_profile文件配置环境变量,增加如下:

export SYSTEM_DEV=true
var configFile = map[string]string{
"PREFIX": "config",
"ENV": "SYSTEM_DEV",
}

func getEnv(key string) bool {
// 检测系统的环境变量
viper.AutomaticEnv()

// 获取指定key值的环境变量
return viper.GetBool(key)
}

func getEnvFile() string {
var fileName string

// 是否是开发环境
isDev := getEnv(configFile["ENV"])

if isDev {
fileName = fmt.Sprintf("./config/%s-dev.yaml", configFile["PREFIX"])
} else {
fileName = fmt.Sprintf("./config/%s-prod.yaml", configFile["PREFIX"])
}

return fileName
}

func Initialize() (*Config, error) {
viper.SetConfigFile(getEnvFile())
// ...
}

动态端口

在开发环境下,一般会指定一个端口启动项目,因为在开发环境下,端口占用一般是较为明确的,明确端口才能方便进行API和服务的测试。

在生产环境下,服务端口占用非常多,且动态端口占用的情况也比较多。所以在服务器上,使用端口往往是不明确的,固定设置的端口,很有可能已经被其他项目占用了。如果一个一个的查询端口占用情况,将花费很多时间,而且端口一般是动态分配的,所以预期的端口不一定随时都可以使用。

因此,在开发环境下,需要配置固定的端口号,而在生产环境下,通过获取空闲端口,动态分配端口号。

func getPort() (int, error) {
ip, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}

listener, err := net.ListenTCP("tcp", ip)

if err != nil {
return 0, nil
}

defer listener.Close()

return listener.Addr().(*net.TCPAddr).Port, nil
}

func Initialize() (*Config, error) {
viper.SetConfigFile(getEnvFile())
// ...
port, err := getPort()
cfg.UserAPI.PORT = port
}