环境变量与配置管理

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

1. 环境变量基础

1.1 读取环境变量

import (
    "fmt"
    "os"
)

func main() {
    // 读取环境变量
    dbHost := os.Getenv("DB_HOST")
    if dbHost == "" {
        dbHost = "localhost"  // 默认值
    }

    fmt.Println("DB Host:", dbHost)

    // 检查环境变量是否存在
    port, exists := os.LookupEnv("PORT")
    if !exists {
        port = "8080"
    }
}

1.2 设置环境变量

// 在程序中设置(仅影响当前进程)
os.Setenv("API_KEY", "secret123")

// 在 shell 中设置
// export DB_HOST=localhost
// export DB_PORT=3306

2. godotenv:.env 文件

2.1 安装

go get -u github.com/joho/godotenv

2.2 使用

创建 .env 文件:

DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=secret
API_KEY=your-api-key
DEBUG=true

加载配置:

import (
    "github.com/joho/godotenv"
    "log"
    "os"
)

func main() {
    // 加载 .env 文件
    err := godotenv.Load()
    if err != nil {
        log.Println("No .env file found")
    }

    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")

    fmt.Printf("Connecting to %s:%s\n", dbHost, dbPort)
}

2.3 多环境配置

func main() {
    env := os.Getenv("GO_ENV")
    if env == "" {
        env = "development"
    }

    // 根据环境加载不同的配置文件
    godotenv.Load(".env." + env)

    // .env.development
    // .env.production
    // .env.test
}

3. Viper:强大的配置库

3.1 安装

go get -u github.com/spf13/viper

3.2 基础使用

创建 config.yaml

server:
  port: 8080
  host: localhost

database:
  host: localhost
  port: 3306
  user: root
  password: secret
  dbname: myapp

redis:
  host: localhost
  port: 6379

读取配置:

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

func main() {
    // 设置配置文件名和路径
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")

    // 读取配置
    if err := viper.ReadInConfig(); err != nil {
        panic(err)
    }

    // 获取配置值
    port := viper.GetInt("server.port")
    dbHost := viper.GetString("database.host")

    fmt.Printf("Server port: %d\n", port)
    fmt.Printf("DB host: %s\n", dbHost)
}

3.3 结构体绑定

type Config struct {
    Server struct {
        Port int    `mapstructure:"port"`
        Host string `mapstructure:"host"`
    } `mapstructure:"server"`

    Database struct {
        Host     string `mapstructure:"host"`
        Port     int    `mapstructure:"port"`
        User     string `mapstructure:"user"`
        Password string `mapstructure:"password"`
        DBName   string `mapstructure:"dbname"`
    } `mapstructure:"database"`
}

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    viper.ReadInConfig()

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

    fmt.Printf("Server: %s:%d\n", config.Server.Host, config.Server.Port)
}

3.4 环境变量覆盖

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    viper.ReadInConfig()

    // 自动读取环境变量
    viper.AutomaticEnv()

    // 设置环境变量前缀
    viper.SetEnvPrefix("MYAPP")

    // 现在可以通过环境变量覆盖配置
    // export MYAPP_SERVER_PORT=9000

    port := viper.GetInt("server.port")  // 如果设置了环境变量,会使用环境变量的值
}

3.5 监听配置变化

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    viper.ReadInConfig()

    // 监听配置文件变化
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Config file changed:", e.Name)
        // 重新加载配置
    })

    // 程序继续运行...
}

4. 最佳实践

4.1 配置优先级

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

  1. 命令行参数
  2. 环境变量
  3. 配置文件
  4. 默认值
func main() {
    // 1. 设置默认值
    viper.SetDefault("server.port", 8080)

    // 2. 读取配置文件
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    viper.ReadInConfig()

    // 3. 读取环境变量
    viper.AutomaticEnv()

    // 4. 命令行参数(使用 pflag 库)
    // pflag.Int("port", 8080, "Server port")
    // pflag.Parse()
    // viper.BindPFlags(pflag.CommandLine)

    port := viper.GetInt("server.port")
}

4.2 敏感信息管理

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

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

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

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

4.3 配置验证

type Config struct {
    Server struct {
        Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
    }
}

func main() {
    var config Config
    viper.Unmarshal(&config)

    // 使用 validator 库验证
    validate := validator.New()
    if err := validate.Struct(config); err != nil {
        log.Fatal("Invalid config:", err)
    }
}

5. 总结

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

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


相关阅读