前言
在现代 Go 应用开发中,配置管理是构建灵活、可维护服务的基础。Viper 是 Go 生态中最流行的配置管理库之一,它支持多种格式(JSON, YAML, TOML, env, flags 等)、环境变量覆盖、远程配置(etcd/Consul)以及配置热加载。
一、Viper 核心特性
🔹 读取 JSON, TOML, YAML, HCL, envfile 或 Java properties 格式的配置文件。
🔹 设置默认值。
🔹 从命令行标志(flags)读取配置。
🔹 从环境变量读取配置。
🔹 从远程配置系统(etcd 或 Consul)读取并监听配置变更。
🔹 监听配置文件变更,实现热加载(关键功能)。
🔹 读取 io.Reader 中的配置。
🔹 能将配置结构体反序列化到你的 struct 中。
二、环境准备
- 安装依赖
go get github.com/spf13/viper
go get github.com/spf13/cobra # (可选,用于 CLI)
- 创建配置文件
在项目根目录创建 config 文件夹,并添加不同环境的配置文件。
config.dev.yaml (测试环境配置)
server:
port: 8080
read_timeout: 60 # 从连接被接受(accept)到请求体被完全读取的时间限制
write_timeout: 60 # 从请求头读取结束到响应写入完成的时间限制
database:
host: "127.0.0.1"
port: 3306
username: "root"
password: "root"
dbname: "go_study"
sslmode: "disable"
redis:
host: "127.0.0.1"
port: 6379
password: ""
db: 0
log:
level: "info"
format: "text" # 或 "text"
# 自定义业务配置
app:
name: "go_study"
version: "1.0.0"
debug_mode: true
max_upload_size: 10485760 # 10MB
config.prod.yaml (生产环境配置)
...
# 自定义业务配置
app:
name: "go_study"
version: "1.0.0"
debug_mode: false
max_upload_size: 10485760 # 10MB
三、初始化 Viper 并读取配置
- 创建配置结构体
定义一个结构体来映射配置文件。
// config/config.go
package config
type Config struct {
Server struct {
Port int `mapstructure:"port"`
ReadTimeout int `mapstructure:"read_timeout"`
WriteTimeout int `mapstructure:"write_timeout"`
} `mapstructure:"server"`
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
} `mapstructure:"database"`
Redis struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
Log struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
} `mapstructure:"log"`
App struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
DebugMode bool `mapstructure:"debug_mode"`
MaxUploadSize int64 `mapstructure:"max_upload_size"`
} `mapstructure:"app"`
}
注意:mapstructure tag 是 Viper 反序列化时使用的标签。
- 初始化 Viper
package initialize
import (
"fmt"
"gin/global"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"os"
)
func InitConfig() {
vp := viper.New()
vp.SetConfigType("yaml") // 设置配置文件类型
vp.AddConfigPath(".") // 添加配置文件搜索路径 . 通常是相对于执行 go run 命令时所在的当前工作目录
//viper.SetEnvPrefix("APP") // 设置环境变量前缀 APP_XXX
// 设置默认值,Viper 会自动将环境变量的值映射到其内部的配置存储中。这意味着,你可以在程序外部(如操作系统、Docker、云平台)通过设置环境变量来覆盖程序内部的配置文件或默认值
vp.SetDefault("server.port", 8080)
vp.SetDefault("log.level", "info")
// 读取环境变量
vp.AutomaticEnv()
env := os.Getenv("GO_APP_ENV")
fmt.Println("GO_APP_ENV:", env)
configName := fmt.Sprintf("config.%s", "prod")
if env == "dev" {
configName = fmt.Sprintf("config.%s", "dev")
}
vp.SetConfigName(configName)
if err := vp.ReadInConfig(); err != nil {
panic(err)
}
if err := vp.Unmarshal(&global.Config); err != nil {
panic(err)
}
}
四、配置热加载(核心功能)
Viper 的 WatchConfig 功能可以监听配置文件的修改事件,并在文件变更时自动重新加载配置。
1. 启用热加载
在 InitConfig 函数末尾添加:
// 启用配置热加载
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Printf("Config file changed: %s", e.Name)
// 重新反序列化配置到结构体
if err := viper.Unmarshal(&global.config); err != nil {
log.Printf("Error re-loading config: %v", err)
return
}
// 可以在这里执行配置变更后的逻辑
// 例如:重启服务器、更新日志级别、刷新缓存等
log.Printf("Config reloaded. New server port: %d", Cfg.Server.Port)
log.Printf("New log level: %s", Cfg.Log.Level)
})
注意:需要导入 github.com/fsnotify/fsnotify,Viper 会自动引入。
2. 热加载的限制与注意事项
- 仅限文件系统变更:WatchConfig 监听的是文件系统事件(修改、创建、删除)。环境变量或远程配置的变更不会触发此事件。
- 结构体需重新反序列化:viper.Unmarshal(&Cfg) 会覆盖整个 Cfg 结构体。确保你的应用逻辑能安全地处理配置变更。
- 非原子性:配置变更的处理不是原子的。如果多个 goroutine 同时读取 Cfg,可能在变更过程中读取到部分旧值和部分新值。对于关键配置,考虑使用 sync.RWMutex 保护 Cfg 结构体。
- 谨慎处理敏感操作:不要在 OnConfigChange 中执行阻塞或危险操作(如关闭数据库连接),除非你完全控制其影响。
五、在 Gin 应用中使用
1. 主程序集成
// main.go
package main
import (
"log"
"net/http"
"time"
"gin/initialize"
"github.com/gin-gonic/gin"
)
func main() {
// 初始化配置(包含热加载)
initialize.InitConfig()
r := gin.Default()
// 使用配置
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
"config": global.Config,
})
})
// 配置 Gin 模式
if config.Cfg.App.DebugMode {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
// 创建 HTTP 服务器
srv := &http.Server{
Addr: ":" + fmt.Sprint(config.Cfg.Server.Port),
Handler: r,
ReadTimeout: time.Duration(config.Cfg.Server.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(config.Cfg.Server.WriteTimeout) * time.Second,
}
// 启动服务器(热加载不影响运行中的服务器端口)
log.Printf("Server starting on port %d", config.Cfg.Server.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}
2. 环境变量覆盖示例
你可以在运行时通过环境变量覆盖配置:
##### 覆盖服务器端口
APP_SERVER_PORT=9000 go run main.go
##### 覆盖日志级别
APP_LOG_LEVEL=debug go run main.go
##### 指定环境(加载 config.prod.yaml)
APP_ENV=prod go run main.go
六、测试热加载
启动应用:go run main.go
访问 http://localhost:8080/ping,观察返回的 port。
修改 config/config.yaml 或 config/config.dev.yaml 中的 server.port 为 8088。
保存文件。
观察终端日志,应出现 Config file changed… 和 Config reloaded… 的提示。
再次访问 http://localhost:8088/ping (端口已变),请求成功。
注意:热加载不会自动重启 HTTP 服务器以监听新端口。上面的例子中,srv.Addr 在服务器启动后是固定的。要实现真正的端口热切换,需要更复杂的逻辑(如优雅重启)。热加载更适合非关键配置(如日志级别、业务参数)。
七、其他用法
1. 监听特定键的变化
viper.OnConfigChange(func(e fsnotify.Event) {
// 检查是否是特定配置项变更
if viper.IsSet("log.level") {
newLevel := viper.GetString("log.level")
// 更新日志库的级别
updateLogLevel(newLevel)
}
})
2. 使用 Viper 直接读取(无需结构体)
port := viper.GetInt("server.port")
level := viper.GetString("log.level")
3. 从远程 etcd/Consul 读取
viper.AddRemoteProvider("etcd", "http://127.0.0.1:2379", "/config/myapp")
viper.SetConfigType("yaml") // 需要指定远程配置的格式
err := viper.ReadRemoteConfig()
viper.WatchRemoteConfig() // 监听远程变更
八、总结
通过 Viper 与 Gin 的集成:
- 灵活的配置读取:支持多格式、多环境、环境变量覆盖。
- 配置热加载:监听文件变更,动态更新应用配置。
- 结构化管理:使用 Go struct 组织配置,类型安全。
热加载是强大功能,但需谨慎使用。它最适合用于调整非关键的运行时参数(日志、缓存、业务规则)。对于数据库连接、服务器端口等关键基础设施配置,建议结合优雅重启 (Graceful Restart) 或配置中心来实现真正的无缝变更。