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 使用以下优先级顺序,每项的优先级高于其下方的项:
- 显式调用
Set。 - 命令行标志。
- 环境变量。
- 配置文件。
- 键/值存储。
- 默认值。
配置与读取
Set/Get
可使用Set和Get方法进行配置的设置和读取:
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的大小写是不敏感的,因此db和DB本质上是设置的同一个key值。
写入文件
可通过WriteConfig、WriteConfigAs、SafeWriteConfig、SafeWriteConfigAs等方法写入配置文件,适合保存、迁移配置文件:
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
}
}
通常通过此种方式将配置解析为结构体。
项目应用
- config.yaml
- config.go
- mysql.go
- main.go
gin:
ip: 0.0.0.0
port: 3300
db:
host: 127.0.0.1
port: 3306
user: root
password: 12345678
db_name: golang
package config
import (
"log"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
// 通过 mapstructure 映射
type GinConfig struct {
Ip string `mapstructure:"ip"`
Port string `mapstructure:"port"`
}
type DBConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"db_name"`
}
// 合并类型,对应配置文件
type Config struct {
*GinConfig `mapstructure:"gin"`
*DBConfig `mapstructure:"db"`
}
// 初始化
var cfg = new(Config)
func Initialize() (*Config, error) {
viper.SetConfigFile("./config/config.yaml")
viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
err := viper.Unmarshal(&cfg)
if err != nil {
log.Fatalf("Failed to init config file: %s", err.Error())
return
}
})
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
// 解析配置文件并匹配Config
e := viper.Unmarshal(cfg)
if e != nil {
return nil, e
}
return cfg, nil
}
package util
import (
"configuration/gin-viper/config"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func DBConnect(cfg *config.DBConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTimeTrue&loc=Local",
cfg.User,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.DBName,
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
package main
import (
"configuration/gin-viper/config"
"configuration/gin-viper/util"
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
cfg, err := config.Initialize()
if err != nil {
log.Fatalf("Failed to initialize config: %s", err.Error())
return
}
db, e := util.DBConnect(cfg.DBConfig)
if e != nil {
log.Fatalf("Failed to connect to mysql")
return
}
r := gin.Default()
if err := r.Run(fmt.Sprintf("%s:%s", cfg.GinConfig.Ip, cfg.GinConfig.Port)); err != nil {
log.Fatalf("Failed to run server: %s", err.Error())
return
}
}
环境变量
Viper可以访问系统环境变量,可以根据环境变量加载不同的应用环境。
Mac下设置环境变量:
打开用户目录下.bash_profile文件配置环境变量,增加如下:
export SYSTEM_DEV=true
- config.go
- config/config-dev.yaml
- config/config-prod.yaml
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())
// ...
}
gin:
ip: 0.0.0.0
port: 8800
db:
host: 127.0.0.1
port: 3306
user: root
password: 12345678
db_name: golang
gin:
ip: 0.0.0.0
port: 3300
db:
host: 127.0.0.1
port: 3306
user: root
password: 12345678
db_name: golang
动态端口
在开发环境下,一般会指定一个端口启动项目,因为在开发环境下,端口占用一般是较为明确的,明确端口才能方便进行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
}