配置管理:让你的应用更灵活
写代码时,你一定遇到过这样的场景:
- 开发环境用本地数据库,生产环境用云数据库,难道要改代码?
- 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 的配置优先级从高到低:
- 显式调用 Set
- 命令行参数(配合 cobra)
- 环境变量
- 配置文件
- 默认值
// 设置默认值
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 的配置管理:
- 环境变量:最简单的配置方式,适合容器化部署
- 配置文件:JSON、YAML、TOML 等格式
- Viper:强大的配置管理库,支持多源、优先级、监听变化
- 最佳实践:敏感信息、多环境、配置验证、热更新
良好的配置管理是应用可维护性的基础。选择合适的配置方案,遵循最佳实践,让你的应用更加灵活和可靠。
练习时间
- 配置加密:实现配置文件中的敏感字段加密存储
- 远程配置:集成 etcd 或 Consul 作为配置中心
- 配置版本管理:记录配置的历史变更
- 配置模板:支持配置文件中的变量替换和模板渲染
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。