第 3 章 · 配置管理与环境变量
本章目标:使用 Viper 与环境变量加载配置;定义 Config struct 集中管理;维护 .env.example;区分 dev/staging/prod 多环境;实践十二要素应用「配置与代码分离」;为 api-go-demo 接入 MySQL、JWT、Redis 配置项;对照 fastapi-web ch03 pydantic-settings 与 flask-web ch03 config 类。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 gin-web ch02。
3.1 场景说明:十二要素配置
api-go-demo 部署在 https://api.example.com(本地 127.0.0.1:8080)。数据库密码、JWT 密钥等敏感信息不得硬编码进源码或提交 Git。
| 配置项 | 环境变量 | yaml 键(Viper) | 示例 |
|---|---|---|---|
| 运行环境 | APP_ENV | app.env | dev / staging / prod |
| 监听端口 | APP_PORT | server.port | 8080 |
| 数据库 | DATABASE_URL | — | MySQL DSN |
| JWT 密钥 | JWT_SECRET | — | 随机 32+ 字节 |
| Redis | REDIS_ADDR | redis.addr | 127.0.0.1:6379 |
| 日志级别 | LOG_LEVEL | log.level | debug / info |
configs/config.yaml ── 非敏感默认值
.env(本地,gitignore) ── 敏感项
K8s Secret / CI 变量 ── 生产注入
对照章节:
| 主题 | flask-web ch03 | fastapi-web ch03 | gin-web ch03 |
|---|---|---|---|
| 配置类 | Config 子类 | BaseSettings | Config struct |
| 环境切换 | FLASK_ENV | APP_ENV | APP_ENV |
| 示例文件 | .env.example | .env.example | .env.example |
3.2 逐步操作表
| 步骤 | 操作 | 验证 |
|---|---|---|
| 1 | go get github.com/spf13/viper@v1.19.0 | go.mod 更新 |
| 2 | 创建 internal/config/config.go | Config struct 定义 |
| 3 | 创建 configs/config.yaml | yaml 可读 |
| 4 | 创建 .env.example | 仓库可提交 |
| 5 | .gitignore 加入 .env | 真实密钥不进 Git |
| 6 | main.go 调用 config.Load() | 缺 DATABASE_URL 时 fail fast |
| 7 | prod 无 JWT_SECRET 拒绝启动 | 报错信息清晰 |
| 8 | APP_PORT=9090 go run | 监听 9090 |
3.3 目录结构
api-go-demo/
├── configs/
│ ├── config.yaml
│ └── config.staging.yaml # 选修
├── .env.example
├── internal/
│ └── config/
│ └── config.go
└── cmd/server/main.go
3.4 Config 结构体(完整)
// internal/config/config.go
package config
import (
"fmt"
"os"
"strings"
"github.com/spf13/viper"
)
type Config struct {
Env string `mapstructure:"env"`
Port string `mapstructure:"port"`
DatabaseURL string `mapstructure:"database_url"`
JWTSecret string `mapstructure:"jwt_secret"`
RedisAddr string `mapstructure:"redis_addr"`
LogLevel string `mapstructure:"log_level"`
CORSOrigins []string
}
func Load() (*Config, error) {
v := viper.New()
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("./configs")
v.AddConfigPath(".")
// 默认值
v.SetDefault("app.env", "dev")
v.SetDefault("server.port", "8080")
v.SetDefault("redis.addr", "127.0.0.1:6379")
v.SetDefault("log.level", "debug")
// 环境变量:SERVER_PORT → server.port
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
_ = v.BindEnv("app.env", "APP_ENV")
_ = v.BindEnv("server.port", "APP_PORT")
_ = v.BindEnv("database_url", "DATABASE_URL")
_ = v.BindEnv("jwt_secret", "JWT_SECRET")
_ = v.BindEnv("redis.addr", "REDIS_ADDR")
_ = v.BindEnv("log.level", "LOG_LEVEL")
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("read config: %w", err)
}
// 配置文件可选,仅靠环境变量也可启动
}
cfg := &Config{
Env: v.GetString("app.env"),
Port: v.GetString("server.port"),
DatabaseURL: v.GetString("database_url"),
JWTSecret: v.GetString("jwt_secret"),
RedisAddr: v.GetString("redis.addr"),
LogLevel: v.GetString("log.level"),
}
cors := os.Getenv("CORS_ORIGINS")
if cors == "" {
cfg.CORSOrigins = []string{"*"}
} else {
cfg.CORSOrigins = strings.Split(cors, ",")
}
return cfg, cfg.Validate()
}
func (c *Config) Validate() error {
if c.DatabaseURL == "" {
return fmt.Errorf("DATABASE_URL is required")
}
if c.Env == "prod" {
if c.JWTSecret == "" || len(c.JWTSecret) < 32 {
return fmt.Errorf("JWT_SECRET must be at least 32 bytes in prod")
}
if c.Port == "" {
return fmt.Errorf("APP_PORT required in prod")
}
}
return nil
}
func (c *Config) IsDev() bool { return c.Env == "dev" }
func (c *Config) IsProd() bool { return c.Env == "prod" }
3.5 configs/config.yaml
# configs/config.yaml — 非敏感默认值,可被环境变量覆盖
app:
env: dev
name: api-go-demo
server:
port: "8080"
redis:
addr: "127.0.0.1:6379"
log:
level: debug
| 规则 | 说明 |
|---|---|
| 敏感项不进 yaml | DATABASE_URL、JWT_SECRET 仅环境变量 |
| 环境变量优先 | APP_PORT=9090 覆盖 yaml 8080 |
| 键名映射 | server.port ↔ SERVER_PORT(AutomaticEnv + Replacer) |
3.6 .env.example
# .env.example — 复制为 .env 后填写,勿提交 .env 到 Git
APP_ENV=dev
APP_PORT=8080
# MySQL DSN(ch04 使用)
DATABASE_URL=api_demo:api_demo_pass@tcp(127.0.0.1:3306)/api_demo?charset=utf8mb4&parseTime=True&loc=Local
# JWT(ch06 使用,生产务必随机)
JWT_SECRET=change-me-use-openssl-rand-hex-32
REDIS_ADDR=127.0.0.1:6379
LOG_LEVEL=debug
# 逗号分隔,prod 示例:https://user.example.com,https://admin.example.com
# CORS_ORIGINS=*