配置管理:让你的应用更灵活

学习 Go 的配置管理方案:环境变量、配置文件、Viper 库的使用和最佳实践

配置管理:让你的应用更灵活

写代码时,你一定遇到过这样的场景:

  • 开发环境用本地数据库,生产环境用云数据库,难道要改代码?
  • API 密钥、数据库密码这些敏感信息能硬编码在代码里吗?
  • 修改一个配置就要重新编译部署吗?

这些问题的答案都是——配置管理

好的配置管理能让你的应用在不同环境(开发、测试、生产)下灵活运行,让敏感信息安全可控,让运维变得轻松。

今天我们就来学习 Go 的配置管理,从简单的环境变量到强大的 Viper 库。

环境变量

环境变量是最简单的配置方式,特别适合容器化部署(如 Docker、Kubernetes)。

基础用法

package main

import (
	"fmt"
	"os"
)

func main() {
	// 读取环境变量
	dbHost := os.Getenv("DB_HOST")
	if dbHost == "" {
		dbHost = "localhost"  // 默认值
	}
	
	dbPort := os.Getenv("DB_PORT")
	if dbPort == "" {
		dbPort = "3306"
	}
	
	fmt.Printf("数据库地址: %s:%s\n", dbHost, dbPort)
}

运行:

$ DB_HOST=example.com DB_PORT=5432 go run main.go
数据库地址: example.com:5432

Lookup:判断是否存在

os.Getenv 无法区分"环境变量不存在"和"环境变量是空字符串"两种情况。os.LookupEnv 可以:

if value, exists := os.LookupEnv("API_KEY"); exists {
	fmt.Println("API_KEY 已配置:", value)
} else {
	fmt.Println("API_KEY 未配置")
}

设置环境变量

os.Setenv("APP_NAME", "MyApp")
fmt.Println(os.Getenv("APP_NAME"))  // MyApp

// 删除环境变量
os.Unsetenv("APP_NAME")

配置文件

对于复杂的配置,配置文件是更好的选择。常见的格式有 JSON、YAML、TOML 等。

JSON 配置文件

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Config struct {
	AppName  string `json:"app_name"`
	Port     int    `json:"port"`
	Debug    bool   `json:"debug"`
	Database struct {
		Host     string `json:"host"`
		Port     int    `json:"port"`
		Username string `json:"username"`
		Password string `json:"password"`
		Name     string `json:"name"`
	} `json:"database"`
	Redis struct {
		Addr     string `json:"addr"`
		Password string `json:"password"`
		DB       int    `json:"db"`
	} `json:"redis"`
	Log struct {
		Level    string `json:"level"`
		Filename string `json:"filename"`
	} `json:"log"`
}

func LoadConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	
	var config Config
	if err := json.Unmarshal(data, &config); err != nil {
		return nil, err
	}
	
	return &config, nil
}

func main() {
	config, err := LoadConfig("config.json")
	if err != nil {
		fmt.Println("加载配置失败:", err)
		return
	}
	
	fmt.Printf("应用: %s (端口 %d)\n", config.AppName, config.Port)
	fmt.Printf("数据库: %s:%d/%s\n",
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)
	fmt.Printf("日志级别: %s\n", config.Log.Level)
}

带默认值的配置加载

func LoadConfigWithDefaults(path string) (*Config, error) {
	// 设置默认值
	config := &Config{
		AppName: "MyApp",
		Port:    8080,
		Debug:   false,
	}
	config.Database.Host = "localhost"
	config.Database.Port = 3306
	config.Log.Level = "info"
	
	// 尝试加载配置文件
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			// 文件不存在,使用默认值
			return config, nil
		}
		return nil, err
	}
	
	// 配置文件会覆盖默认值
	if err := json.Unmarshal(data, config); err != nil {
		return nil, err
	}
	
	return config, nil
}

Viper:强大的配置管理库

Viper 是 Go 社区最流行的配置管理库。它支持多种配置源,并且能自动处理优先级。

安装

go get github.com/spf13/viper

基础使用

package main

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

func main() {
	// 设置配置文件名(不需要扩展名)
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	viper.AddConfigPath("./config")
	viper.AddConfigPath("/etc/myapp")
	
	// 读取配置文件
	if err := viper.ReadInConfig(); err != nil {
		fmt.Println("读取配置失败:", err)
	}
	
	// 获取配置值
	appName := viper.GetString("app_name")
	port := viper.GetInt("port")
	dbHost := viper.GetString("database.host")
	
	fmt.Printf("应用: %s (端口 %d)\n", appName, port)
	fmt.Printf("数据库: %s\n", dbHost)
}

配置优先级

Viper 的配置优先级从高到低:

  1. 显式调用 Set
  2. 命令行参数(配合 cobra)
  3. 环境变量
  4. 配置文件
  5. 默认值
// 设置默认值
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 3306)

// 绑定环境变量
viper.SetEnvPrefix("MYAPP")  // 环境变量前缀
viper.AutomaticEnv()          // 自动读取环境变量

// 现在 MYAPP_PORT=9090 会覆盖配置文件中的 port

// 显式设置(最高优先级)
viper.Set("debug", true)

绑定结构体

type Config struct {
	AppName  string `mapstructure:"app_name"`
	Port     int    `mapstructure:"port"`
	Debug    bool   `mapstructure:"debug"`
	Database struct {
		Host     string `mapstructure:"host"`
		Port     int    `mapstructure:"port"`
		Username string `mapstructure:"username"`
		Password string `mapstructure:"password"`
	} `mapstructure:"database"`
}

var config Config
if err := viper.Unmarshal(&config); err != nil {
	log.Fatal(err)
}

fmt.Printf("数据库: %s:%d\n", config.Database.Host, config.Database.Port)

监听配置变化

Viper 可以监听配置文件的变化,自动重新加载:

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("配置文件已更新:", e.Name)
	
	// 重新加载配置
	var newConfig Config
	viper.Unmarshal(&newConfig)
	
	// 应用新配置
	applyConfig(newConfig)
})

实战:完整的配置系统

让我们实现一个生产级的配置系统:

package main

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

type AppConfig struct {
	// 应用信息
	AppName    string `mapstructure:"app_name"`
	Version    string `mapstructure:"version"`
	Env        string `mapstructure:"env"`
	Debug      bool   `mapstructure:"debug"`
	
	// 服务器配置
	Server struct {
		Host         string        `mapstructure:"host"`
		Port         int           `mapstructure:"port"`
		ReadTimeout  time.Duration `mapstructure:"read_timeout"`
		WriteTimeout time.Duration `mapstructure:"write_timeout"`
	} `mapstructure:"server"`
	
	// 数据库配置
	Database struct {
		Driver       string `mapstructure:"driver"`
		Host         string `mapstructure:"host"`
		Port         int    `mapstructure:"port"`
		Username     string `mapstructure:"username"`
		Password     string `mapstructure:"password"`
		Name         string `mapstructure:"name"`
		MaxOpenConns int    `mapstructure:"max_open_conns"`
		MaxIdleConns int    `mapstructure:"max_idle_conns"`
	} `mapstructure:"database"`
	
	// Redis 配置
	Redis struct {
		Addr     string `mapstructure:"addr"`
		Password string `mapstructure:"password"`
		DB       int    `mapstructure:"db"`
	} `mapstructure:"redis"`
	
	// 日志配置
	Log struct {
		Level      string `mapstructure:"level"`
		Format     string `mapstructure:"format"`
		Filename   string `mapstructure:"filename"`
		MaxSize    int    `mapstructure:"max_size"`
		MaxBackups int    `mapstructure:"max_backups"`
		MaxAge     int    `mapstructure:"max_age"`
	} `mapstructure:"log"`
	
	// JWT 配置
	JWT struct {
		Secret          string        `mapstructure:"secret"`
		ExpirationHours int           `mapstructure:"expiration_hours"`
		Issuer          string        `mapstructure:"issuer"`
	} `mapstructure:"jwt"`
}

func LoadConfig() (*AppConfig, error) {
	v := viper.New()
	
	// 设置默认值
	v.SetDefault("app_name", "MyApp")
	v.SetDefault("version", "1.0.0")
	v.SetDefault("env", "development")
	v.SetDefault("debug", false)
	
	v.SetDefault("server.host", "0.0.0.0")
	v.SetDefault("server.port", 8080)
	v.SetDefault("server.read_timeout", 15*time.Second)
	v.SetDefault("server.write_timeout", 15*time.Second)
	
	v.SetDefault("database.driver", "mysql")
	v.SetDefault("database.host", "localhost")
	v.SetDefault("database.port", 3306)
	v.SetDefault("database.max_open_conns", 25)
	v.SetDefault("database.max_idle_conns", 10)
	
	v.SetDefault("redis.addr", "localhost:6379")
	v.SetDefault("redis.db", 0)
	
	v.SetDefault("log.level", "info")
	v.SetDefault("log.format", "json")
	v.SetDefault("log.max_size", 100)
	v.SetDefault("log.max_backups", 30)
	v.SetDefault("log.max_age", 7)
	
	v.SetDefault("jwt.expiration_hours", 24)
	v.SetDefault("jwt.issuer", "myapp")
	
	// 配置文件
	v.SetConfigName("config")
	v.SetConfigType("yaml")
	v.AddConfigPath(".")
	v.AddConfigPath("./config")
	v.AddConfigPath("/etc/myapp")
	
	// 环境变量
	v.SetEnvPrefix("MYAPP")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.AutomaticEnv()
	
	// 读取配置文件
	if err := v.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			return nil, fmt.Errorf("读取配置失败: %w", err)
		}
		log.Println("未找到配置文件,使用默认值和环境变量")
	} else {
		log.Printf("使用配置文件: %s", v.ConfigFileUsed())
	}
	
	// 绑定到结构体
	var config AppConfig
	if err := v.Unmarshal(&config); err != nil {
		return nil, fmt.Errorf("解析配置失败: %w", err)
	}
	
	// 验证配置
	if err := validateConfig(&config); err != nil {
		return nil, fmt.Errorf("配置验证失败: %w", err)
	}
	
	return &config, nil
}

func validateConfig(config *AppConfig) error {
	if config.Database.Name == "" {
		return fmt.Errorf("database.name 不能为空")
	}
	
	if config.JWT.Secret == "" {
		// 生产环境必须有 JWT 密钥
		if config.Env == "production" {
			return fmt.Errorf("jwt.secret 在生产环境中不能为空")
		}
		// 开发环境使用默认值
		config.JWT.Secret = "dev-secret-key"
	}
	
	return nil
}

func (c *AppConfig) Print() {
	fmt.Println("=== 应用配置 ===")
	fmt.Printf("应用名称: %s v%s\n", c.AppName, c.Version)
	fmt.Printf("环境: %s (debug=%v)\n", c.Env, c.Debug)
	fmt.Printf("服务器: %s:%d\n", c.Server.Host, c.Server.Port)
	fmt.Printf("数据库: %s://%s:%d/%s\n",
		c.Database.Driver,
		c.Database.Host,
		c.Database.Port,
		c.Database.Name)
	fmt.Printf("Redis: %s\n", c.Redis.Addr)
	fmt.Printf("日志级别: %s\n", c.Log.Level)
}

// DSN 返回数据库连接字符串
func (c *AppConfig) DSN() string {
	return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
		c.Database.Username,
		c.Database.Password,
		c.Database.Host,
		c.Database.Port,
		c.Database.Name)
}

func main() {
	config, err := LoadConfig()
	if err != nil {
		log.Fatal(err)
	}
	
	config.Print()
	
	// 使用配置
	fmt.Println("\n数据库 DSN:", config.DSN())
}

配置文件示例

# config.yaml
app_name: MyApp
version: 1.0.0
env: development
debug: true

server:
  host: 0.0.0.0
  port: 8080
  read_timeout: 30s
  write_timeout: 30s

database:
  driver: mysql
  host: localhost
  port: 3306
  username: root
  password: password
  name: myapp_dev
  max_open_conns: 25
  max_idle_conns: 10

redis:
  addr: localhost:6379
  password: ""
  db: 0

log:
  level: debug
  format: text
  filename: logs/app.log

jwt:
  secret: dev-secret-key-change-in-production
  expiration_hours: 24
  issuer: myapp

生产环境配置

# config.production.yaml
app_name: MyApp
version: 1.0.0
env: production
debug: false

server:
  port: 80

database:
  host: db.example.com
  username: myapp_prod
  # password 通过环境变量 MYAPP_DATABASE_PASSWORD 注入
  name: myapp_prod

log:
  level: warn
  format: json
  filename: /var/log/myapp/app.log

配置最佳实践

1. 敏感信息使用环境变量

# 不要在配置文件中存储密码
MYAPP_DATABASE_PASSWORD=secret go run main.go

2. 多环境配置

env := os.Getenv("APP_ENV")
if env == "" {
	env = "development"
}

viper.SetConfigName(fmt.Sprintf("config.%s", env))

3. 配置验证

func validateConfig(c *AppConfig) error {
	if c.Server.Port < 1 || c.Server.Port > 65535 {
		return fmt.Errorf("无效的端口号: %d", c.Server.Port)
	}
	
	if c.Database.MaxOpenConns < c.Database.MaxIdleConns {
		return fmt.Errorf("max_open_conns 必须大于等于 max_idle_conns")
	}
	
	return nil
}

4. 配置热更新

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
	log.Printf("配置已更新: %s", e.Name)
	
	var newConfig AppConfig
	if err := viper.Unmarshal(&newConfig); err != nil {
		log.Printf("重新加载配置失败: %v", err)
		return
	}
	
	// 应用新配置(比如重新连接数据库)
	applyConfig(newConfig)
})

小结

今天我们学习了 Go 的配置管理:

  1. 环境变量:最简单的配置方式,适合容器化部署
  2. 配置文件:JSON、YAML、TOML 等格式
  3. Viper:强大的配置管理库,支持多源、优先级、监听变化
  4. 最佳实践:敏感信息、多环境、配置验证、热更新

良好的配置管理是应用可维护性的基础。选择合适的配置方案,遵循最佳实践,让你的应用更加灵活和可靠。

练习时间

  1. 配置加密:实现配置文件中的敏感字段加密存储
  2. 远程配置:集成 etcd 或 Consul 作为配置中心
  3. 配置版本管理:记录配置的历史变更
  4. 配置模板:支持配置文件中的变量替换和模板渲染

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页