环境变量与配置管理

同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。

1. 环境变量基础

1.1 读取环境变量

 1import (
 2    "fmt"
 3    "os"
 4)
 5
 6func main() {
 7    // 读取环境变量
 8    dbHost := os.Getenv("DB_HOST")
 9    if dbHost == "" {
10        dbHost = "localhost"  // 默认值
11    }
12
13    fmt.Println("DB Host:", dbHost)
14
15    // 检查环境变量是否存在
16    port, exists := os.LookupEnv("PORT")
17    if !exists {
18        port = "8080"
19    }
20}

1.2 设置环境变量

1// 在程序中设置(仅影响当前进程)
2os.Setenv("API_KEY", "secret123")
3
4// 在 shell 中设置
5// export DB_HOST=localhost
6// export DB_PORT=3306

2. godotenv:.env 文件

2.1 安装

1go get -u github.com/joho/godotenv

2.2 使用

创建 .env 文件:

1DB_HOST=localhost
2DB_PORT=3306
3DB_USER=root
4DB_PASSWORD=secret
5API_KEY=your-api-key
6DEBUG=true

加载配置:

 1import (
 2    "github.com/joho/godotenv"
 3    "log"
 4    "os"
 5)
 6
 7func main() {
 8    // 加载 .env 文件
 9    err := godotenv.Load()
10    if err != nil {
11        log.Println("No .env file found")
12    }
13
14    dbHost := os.Getenv("DB_HOST")
15    dbPort := os.Getenv("DB_PORT")
16
17    fmt.Printf("Connecting to %s:%s\n", dbHost, dbPort)
18}

2.3 多环境配置

 1func main() {
 2    env := os.Getenv("GO_ENV")
 3    if env == "" {
 4        env = "development"
 5    }
 6
 7    // 根据环境加载不同的配置文件
 8    godotenv.Load(".env." + env)
 9
10    // .env.development
11    // .env.production
12    // .env.test
13}

3. Viper:强大的配置库

3.1 安装

1go get -u github.com/spf13/viper

3.2 基础使用

创建 config.yaml

 1server:
 2  port: 8080
 3  host: localhost
 4
 5database:
 6  host: localhost
 7  port: 3306
 8  user: root
 9  password: secret
10  dbname: myapp
11
12redis:
13  host: localhost
14  port: 6379

读取配置:

 1import (
 2    "fmt"
 3    "github.com/spf13/viper"
 4)
 5
 6func main() {
 7    // 设置配置文件名和路径
 8    viper.SetConfigName("config")
 9    viper.SetConfigType("yaml")
10    viper.AddConfigPath(".")
11    viper.AddConfigPath("./config")
12
13    // 读取配置
14    if err := viper.ReadInConfig(); err != nil {
15        panic(err)
16    }
17
18    // 获取配置值
19    port := viper.GetInt("server.port")
20    dbHost := viper.GetString("database.host")
21
22    fmt.Printf("Server port: %d\n", port)
23    fmt.Printf("DB host: %s\n", dbHost)
24}

3.3 结构体绑定

 1type Config struct {
 2    Server struct {
 3        Port int    `mapstructure:"port"`
 4        Host string `mapstructure:"host"`
 5    } `mapstructure:"server"`
 6
 7    Database struct {
 8        Host     string `mapstructure:"host"`
 9        Port     int    `mapstructure:"port"`
10        User     string `mapstructure:"user"`
11        Password string `mapstructure:"password"`
12        DBName   string `mapstructure:"dbname"`
13    } `mapstructure:"database"`
14}
15
16func main() {
17    viper.SetConfigName("config")
18    viper.AddConfigPath(".")
19    viper.ReadInConfig()
20
21    var config Config
22    if err := viper.Unmarshal(&config); err != nil {
23        panic(err)
24    }
25
26    fmt.Printf("Server: %s:%d\n", config.Server.Host, config.Server.Port)
27}

3.4 环境变量覆盖

 1func main() {
 2    viper.SetConfigName("config")
 3    viper.AddConfigPath(".")
 4    viper.ReadInConfig()
 5
 6    // 自动读取环境变量
 7    viper.AutomaticEnv()
 8
 9    // 设置环境变量前缀
10    viper.SetEnvPrefix("MYAPP")
11
12    // 现在可以通过环境变量覆盖配置
13    // export MYAPP_SERVER_PORT=9000
14
15    port := viper.GetInt("server.port")  // 如果设置了环境变量,会使用环境变量的值
16}

3.5 监听配置变化

 1func main() {
 2    viper.SetConfigName("config")
 3    viper.AddConfigPath(".")
 4    viper.ReadInConfig()
 5
 6    // 监听配置文件变化
 7    viper.WatchConfig()
 8    viper.OnConfigChange(func(e fsnotify.Event) {
 9        fmt.Println("Config file changed:", e.Name)
10        // 重新加载配置
11    })
12
13    // 程序继续运行...
14}

4. 最佳实践

4.1 配置优先级

通常的优先级(从高到低):

  1. 命令行参数
  2. 环境变量
  3. 配置文件
  4. 默认值
 1func main() {
 2    // 1. 设置默认值
 3    viper.SetDefault("server.port", 8080)
 4
 5    // 2. 读取配置文件
 6    viper.SetConfigName("config")
 7    viper.AddConfigPath(".")
 8    viper.ReadInConfig()
 9
10    // 3. 读取环境变量
11    viper.AutomaticEnv()
12
13    // 4. 命令行参数(使用 pflag 库)
14    // pflag.Int("port", 8080, "Server port")
15    // pflag.Parse()
16    // viper.BindPFlags(pflag.CommandLine)
17
18    port := viper.GetInt("server.port")
19}

4.2 敏感信息管理

不要将敏感信息提交到版本控制

# .gitignore
.env
.env.local
.env.*.local
config/production.yaml

使用环境变量或密钥管理服务

1// 生产环境从环境变量读取
2dbPassword := os.Getenv("DB_PASSWORD")
3if dbPassword == "" {
4    // 或从 AWS Secrets Manager / HashiCorp Vault 读取
5}

4.3 配置验证

 1type Config struct {
 2    Server struct {
 3        Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
 4    }
 5}
 6
 7func main() {
 8    var config Config
 9    viper.Unmarshal(&config)
10
11    // 使用 validator 库验证
12    validate := validator.New()
13    if err := validate.Struct(config); err != nil {
14        log.Fatal("Invalid config:", err)
15    }
16}

5. 总结

  • 简单场景:使用 os.Getenv + godotenv
  • 复杂场景:使用 Viper
  • 敏感信息:使用环境变量,不要提交到代码仓库
  • 多环境:使用不同的配置文件或环境变量

思考题: 为什么敏感信息(如数据库密码)不应该写在配置文件中?有哪些更安全的方案?


相关阅读