Skip to content

config

引入 Go 生态中最强大的配置管理神器:Viper

🚀 目标:实现配置分离

我们要达到的效果是:

  1. 创建一个 config.yaml 文件存放配置。
  2. 代码自动读取文件。
  3. 高阶技巧:允许通过环境变量覆盖配置(比如在 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 可能读不进去数据,导致配置为空。

🧪 验证时刻

  1. 确保 config.yaml 在你的项目根目录下。

  2. 运行程序: PowerShell

    go run cmd/api-server/main.go
  3. 看控制台输出:

    • 如果看到 Config loaded: Port=8080...,说明文件读取成功!
    • 尝试修改 config.yaml 里的端口为 9090,重启程序,看端口是否变了。