下载工作台
Gin Web 开发

配置管理与环境变量

试读上半部分 · 解锁后可读全文

第 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_ENVapp.envdev / staging / prod
监听端口APP_PORTserver.port8080
数据库DATABASE_URLMySQL DSN
JWT 密钥JWT_SECRET随机 32+ 字节
RedisREDIS_ADDRredis.addr127.0.0.1:6379
日志级别LOG_LEVELlog.leveldebug / info
configs/config.yaml     ── 非敏感默认值
.env(本地,gitignore)  ── 敏感项
K8s Secret / CI 变量     ── 生产注入

对照章节

主题flask-web ch03fastapi-web ch03gin-web ch03
配置类Config 子类BaseSettingsConfig struct
环境切换FLASK_ENVAPP_ENVAPP_ENV
示例文件.env.example.env.example.env.example

3.2 逐步操作表

步骤操作验证
1go get github.com/spf13/viper@v1.19.0go.mod 更新
2创建 internal/config/config.goConfig struct 定义
3创建 configs/config.yamlyaml 可读
4创建 .env.example仓库可提交
5.gitignore 加入 .env真实密钥不进 Git
6main.go 调用 config.Load()缺 DATABASE_URL 时 fail fast
7prod 无 JWT_SECRET 拒绝启动报错信息清晰
8APP_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
规则说明
敏感项不进 yamlDATABASE_URLJWT_SECRET 仅环境变量
环境变量优先APP_PORT=9090 覆盖 yaml 8080
键名映射server.portSERVER_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=*

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。