config
引入 Go 生态中最强大的配置管理神器:Viper。
🚀 目标:实现配置分离
我们要达到的效果是:
- 创建一个
config.yaml文件存放配置。 - 代码自动读取文件。
- 高阶技巧:允许通过环境变量覆盖配置(比如在 Docker 中部署时)。
第一步:安装 Viper
Viper 是 Go 社区的标准配置库,支持 JSON, YAML, TOML, ENV 等多种格式。
在项目根目录终端执行:
Bash
go get github.com/spf13/viper第二步:编写配置文件
在项目根目录下创建一个名为 config.yaml 的文件。 (YAML 格式比 JSON 更适合写配置,清晰且支持注释)
YAML
server:
port: "8080"
mode: "debug" # debug, release
database:
# 注意:这里换成你自己的真实账号密码
dsn: "root:123456@tcp(127.0.0.1:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
max_idle_conns: 10
max_open_conns: 100第三步:编写配置加载代码
我们需要一个专门的地方来定义配置的结构体(Struct),并负责加载它。
创建文件:internal/config/config.go
Go
package config
import (
"fmt"
"github.com/spf13/viper"
)
// Config 全局配置结构体
// mapstructure 标签用于告诉 Viper 如何将 YAML 的字段映射到结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DBConfig `mapstructure:"database"`
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type DBConfig struct {
DSN string `mapstructure:"dsn"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
}
// LoadConfig 加载配置
// path: 配置文件所在的路径 (例如 ".")
func LoadConfig(path string) (*Config, error) {
// 1. 设置 Viper
viper.AddConfigPath(path) // 告诉 Viper 去哪里找文件
viper.SetConfigName("config") // 文件名 (不需要后缀)
viper.SetConfigType("yaml") // 文件类型
// 2. 读取配置
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// 3. 将读取到的配置反序列化到结构体中
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &config, nil
}第四步:重构 Main 函数
现在我们去 cmd/api-server/main.go,把那些丑陋的硬编码字符串删掉,换成优雅的配置读取。
Go
package main
import (
"fmt"
"log"
"user-system/internal/config" // 引入 config 包
"user-system/internal/handler"
"user-system/internal/service"
"user-system/pkg/database"
"user-system/internal/model" // 确保引入model以便迁移
"github.com/gin-gonic/gin"
)
func main() {
// 1. 加载配置
// "." 表示在当前目录下寻找 config.yaml
conf, err := config.LoadConfig(".")
if err != nil {
log.Panic("Failed to load config: ", err)
}
// 打印一下看看是否成功 (生产环境不要打印密码!)
fmt.Printf("Config loaded: Port=%s, DB-MaxIdle=%d\n", conf.Server.Port, conf.Database.MaxIdleConns)
// 2. 设置 Gin 的运行模式 (Debug / Release)
gin.SetMode(conf.Server.Mode)
// 3. 初始化数据库 (使用配置里的参数)
database.InitMySQL(conf.Database.DSN)
// 此时你可以去 pkg/database/mysql.go 把 SetMaxIdleConns 里的硬编码数字也改成 conf.Database.MaxIdleConns
// 这里为了演示简单先不动 database 包的内部逻辑,但实际上应该透传进去
database.DB.AutoMigrate(&model.User{})
// 4. 初始化各层
userService := service.NewUserService()
userHandler := handler.NewUserHandler(userService)
// 5. 注册路由
r := gin.Default()
r.POST("/users", userHandler.CreateUser)
r.GET("/users/:id", userHandler.GetUser)
r.GET("/users", userHandler.ListUsers)
// 6. 启动服务 (使用配置里的端口)
addr := ":" + conf.Server.Port
fmt.Printf("Starting server on %s...\n", addr)
r.Run(addr)
}🛑 架构师经验:配置管理的“三大坑”
1. 相对路径的陷阱
- 代码里写:
LoadConfig(".") - 问题:如果你在项目根目录运行
go run cmd/api-server/main.go,它是正常的。但如果你进入cmd目录运行,它就会报错找不到文件。 - 原理:
"."指的是运行命令时的当前目录,而不是 main.go 文件所在的目录。 - 解决:在 Docker 或生产部署时,通常指定绝对路径,或者确保工作目录正确。
2. 环境变量覆盖 (Env Override) —— 必学技能!
在生产环境(K8s/Docker)中,我们通常不修改文件,而是通过环境变量注入密码。 Viper 原生支持这个,但需要多写两行代码。
在 LoadConfig 函数中加入:
Go
func LoadConfig(path string) (*Config, error) {
// ... (前面的代码)
// 开启环境变量自动读取
viper.AutomaticEnv()
// 读取文件
viper.ReadInConfig()
// ...
}效果: 如果你在系统里设置了一个环境变量 DATABASE.DSN="new_password...",Viper 会优先使用环境变量的值,覆盖 config.yaml 里的值。这才是云原生应用的标准姿势。
3. 结构体标签 (mapstructure)
- JSON 用
json:"name" - GORM 用
gorm:"column:name" - Viper 用
mapstructure:"name" - 坑:如果你忘了写
mapstructure标签,且结构体字段名和 YAML 键名大小写不一致(YAML通常全小写,Go结构体全大写),Viper 可能读不进去数据,导致配置为空。
🧪 验证时刻
确保
config.yaml在你的项目根目录下。运行程序: PowerShell
go run cmd/api-server/main.go看控制台输出:
- 如果看到
Config loaded: Port=8080...,说明文件读取成功! - 尝试修改
config.yaml里的端口为9090,重启程序,看端口是否变了。
- 如果看到