go-zero 怎么区分 dev / test / prod 环境

「启动的时候怎么区分环境,让 dev、test、prod 用不同的配置?」—— 这篇讲清 go-zero 的多环境机制:多份配置文件 + -f 启动参数(官方首选)、环境变量 ${ENV} 占位、以及 config 里的 Mode 字段,并给出推荐落地组合与启动命令。

一句话结论 为什么需要 方式一·多文件 -f 方式二·环境变量 方式三·Mode 字段 推荐组合 启动命令 代码里读环境 规范与踩坑 框架对比

一句话结论

先把最关键的说在前面。

go-zero 没有 Spring 那种「profile」开关,它的多环境是「配置文件 + 启动参数」模式:为每个环境准备一份 etc/xxx-*.yaml,启动时用 -f 指定用哪份。再叠加两点增强:① 用 ${ENV} 占位符让敏感/部署相关的值走环境变量;② 在 config 里写 Mode: dev/test/pro 控制日志格式等框架行为。

所以「区分环境」= 一份二进制 + 多份配置文件 + 启动参数选文件。这和 go-zero 一贯的「配置外置、代码不写死」风格一脉相承(呼应之前的 config 篇)。

为什么需要区分环境

同一份代码,不同环境「变」的是配置,不变的是逻辑。

连接类差异
MySQL / Redis / etcd 的地址、账号、库名;dev 用本地、prod 用集群。
开关类差异
日志级别/格式(dev 看控制台、prod 出 JSON)、链路采样率(prod 不能全采)、熔断阈值。
密钥类差异
第三方 API Key、JWT 私钥;prod 的密钥绝不能进代码仓库。
行为类差异
是否连真实支付、是否发短信、mock 开关;test 环境常要隔离外部副作用。
核心原则:环境差异只改配置,不改代码。凡是「在代码里写死 env 判断再去连不同库」的,都是反面模式——go-zero 的 f 参数 + 配置就是为消灭它而生的。

多份配置文件 + -f 启动参数

go-zero 原生支持,最干净、最常用。

① 准备多份配置(同结构、不同值)

etc/ ├── user-api.yaml # 默认(不传 -f 时用) ├── user-api.dev.yaml # 开发:本地库、控制台日志、全量采样 ├── user-api.test.yaml # 测试:测试库、mock 开关 └── user-api.prod.yaml # 生产:集群库、JSON 日志、低采样、密钥走 env

② main.go 用 -f 接收配置路径

package main

import (
    "flag"
    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/rest"
    "your_project/internal/config"
    "your_project/internal/handler"
    "your_project/internal/svc"
)

// -f 指定配置文件,默认值指向 etc 目录下的 yaml
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")

func main() {
    flag.Parse()

    var c config.Config
    // MustLoad 读 -f 指向的文件;多环境只换文件、不换代码
    conf.MustLoad(*configFile, &c, conf.UseEnv())

    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    ctx := svc.NewServiceContext(c)
    handler.RegisterHandlers(server, ctx)

    server.Start()
}
goctl 生成的服务,main.go天生就带这个 -f flag,你不用自己写——你只需要把多份 yaml 准备好、启动时候传对路径即可。

③ 三份 yaml 长啥样(只列差异项)

# user-api.dev.yaml
Name: user-api
Mode: dev
Host: 0.0.0.0
Port: 8888
Mysql:
  DataSource: root:123456@tcp(127.0.0.1:3306)/user_dev
CacheRedis:
  - Host: 127.0.0.1:6379
Trace:
  Sampler: 1.0        # dev 全采样,方便排查

# user-api.prod.yaml
Name: user-api
Mode: pro
Host: 0.0.0.0
Port: 8888
Mysql:
  DataSource: ${MYSQL_DSN}   # 密钥/地址走环境变量,不进仓库
CacheRedis:
  - Host: ${REDIS_HOST}
Trace:
  Sampler: 0.1        # prod 低采样,控成本

环境变量 ${ENV} 占位

让「同一份模板」在部署时按环境变量填值。

关键:conf.UseEnv()

go-zero 的 conf 包支持在 yaml 里写 ${VAR},启动时替换为进程环境变量——但前提是 MustLoad 带上 conf.UseEnv() 选项(上面 main.go 已带上)。

# 一份 prod 模板 etc/user-api.prod.yaml
Mysql:
  DataSource: ${MYSQL_DSN}
CacheRedis:
  - Host: ${REDIS_HOST}
Auth:
  AccessSecret: ${JWT_SECRET}   # 私钥绝不进仓库
# 启动时注入环境变量即可(shell / Docker / K8s Secret)
MYSQL_DSN='prod_user:xxxx@tcp(db.prod:3306)/user' \
REDIS_HOST=redis.prod:6379 \
JWT_SECRET='***' \
./user-api -f etc/user-api.prod.yaml
两个互补用法:整份文件按环境切(方式一) + 同一文件里的敏感/部署值用 ${ENV} 覆盖(方式二)。生产环境最佳实践是「prod 模板进仓库 + 真实值全走 env」。
${VAR} 取不到且没设默认值时,go-zero 直接加载失败(fail-fast),这反而是好事——不会带着空密码悄悄连上库。可写 ${VAR:-default} 给兜底默认值(取决于 go-zero 版本是否支持该语法,谨慎使用)。

config 里的 Mode 字段

go-zero 内置的「行为开关」,不是配置切换,是运行时行为切换。

它管什么

rest.RestConf(内嵌 service.ServiceConf)有个 Mode 字符串字段。go-zero 用它决定日志格式 / 运行时行为

Mode 取值(注意不是 "prod"!): dev → 开发:日志更详细、控制台友好 test → 测试:同 dev 侧重 pro → 生产:日志精简、JSON 化、性能优先 # 实际写进 yaml 顶层 Name: user-api Mode: pro # 不是 "prod"!go-zero 只认 dev/test/pro
最容易写错的点:go-zero 的常量是 "dev" / "test" / "pro"没有 "prod"。你要是写成 Mode: prod,框架不认、按默认(非 pro)行为跑,日志格式和 prod 预期不一致。统一用 pro

配合 Log 字段更精细

Log:
  Mode: file          # console / file / volatile
  Encoding: json     # flat(dev 友好) / json(pro 友好,便于采集)
  Path: /var/log/user-api
  Level: info
ModeLog.Encoding 配合:dev 用 flat+控制台、pro 用 json+文件,让日志既好读又好被 Loki/ES 采集。

推荐的落地组合

三种方式不是三选一,而是叠加。

同一份二进制 go build 一次 -f etc/xxx.{env}.yaml ①按环境选配置文件 yaml 内 ${ENV} + Mode ②密钥走 env ③框架行为开关 结果:dev/test/prod 同一二进制、不同配置与运行时行为,零代码改动
① 选文件
-f etc/xxx.dev.yaml 决定用哪套连接/开关
② 填变量
文件里 ${MYSQL_DSN} 等用部署环境变量覆盖
③ 行为开关
Mode: dev/test/pro 控制日志格式与采样

各环境怎么启动

本地、Docker、K8s 三处怎么传 -f

本地 / 裸机

# 开发
go run user.go -f etc/user-api.dev.yaml
# 测试
./user-api -f etc/user-api.test.yaml
# 生产
./user-api -f etc/user-api.prod.yaml

Docker

# Dockerfile
ENTRYPOINT ["/app/user-api", "-f", "/app/etc/user-api.yaml"]
# 不同环境用不同的配置文件挂载(ConfigMap / 构建参数),或传环境变量覆盖 ${ENV}

Kubernetes

# deployment.yaml —— 用 env 注入密钥,配置文件由 ConfigMap 挂到 /app/etc
env:
  - name: MYSQL_DSN
    valueFrom: { secretKeyRef: { name: user-secret, key: dsn } }
  - name: JWT_SECRET
    valueFrom: { secretKeyRef: { name: user-secret, key: jwt } }
# 容器启动参数 -f /app/etc/user-api.prod.yaml(prod 模板已含 ${MYSQL_DSN} 等)
容器里推荐「配置文件用 ConfigMap 挂进 /app/etc + 敏感值用 Secret 转 env」。这样镜像一次构建,dev/test/prod 只换挂载与 env,符合 12-factor。

在代码里怎么读环境

能不读就别读;真要区分,集中在 svc 里。

从 config 拿 Mode

// config.go —— 把 Mode 透传到业务可访问的地方
type Config struct {
    rest.RestConf          // 内嵌了 Mode 字段
    Auth struct {
        AccessSecret string
        AccessExpire int64
    }
}

// svc 中需要时用(尽量克制)
if c.Mode == "pro" {
    // 生产才开的开关,如关闭调试接口
}
反模式提醒:满篇 if c.Mode == "dev" 等于把环境差异又写回代码,违背「配置外置」初衷。能用配置项表达的差异(库地址、采样率、日志级别)一律放 yaml,Mode 只做框架行为控制,业务里尽量别依赖它。

不要自己 new 一个全局 env 变量

呼应「go-zero 全局变量」那篇:环境信息只在启动时读一次、通过 configsvc 注入,别在包里写 var Env = "prod" 全局变量。需要环境相关行为时,在 svc 装配时决定并传依赖进去(比如 dev 注入 mock 客户端、pro 注入真实客户端)。

规范清单

照着做,多环境不翻车。

该做 / 别做说明
✅ 配置外置、代码不写死环境差异全进 yaml,启动用 -f 选;逻辑里零 env 判断
✅ prod 密钥走 ${ENV}账号/私钥用环境变量注入,绝不进 git 仓库
✅ 一份构建产物多环境复用镜像 build 一次,dev/test/prod 只换挂载与 env
⚠️ Modepro 不写 prodgo-zero 只认 dev/test/pro,写错框架不按 prod 行为跑
❌ 别把 prod 配置文件明文提交prod 模板可提交,但真实值用 env;或用加密配置管理
❌ 别在 logic 里散落 if Mode环境差异回代码 = 反面模式,集中在 svc 装配决定
⚠️ 测试环境别开全量采样Sampler: 1.0 仅 dev;test/prod 按量调低,控成本
⚠️ 别用全局 var 存环境环境信息随 config 进 svc,不要包级全局变量

和其他框架比,go-zero 这套啥特点

目标一致(配置随环境变),机制不同。

方案环境切换方式特点
go-zero(本文)多份 yaml + -f 参数 + ${ENV} + Mode无 profile 概念,纯文件 + 参数,最贴近 12-factor
Spring Bootspring.profiles.active=dev + application-{profile}.yml内置 profile 机制,自动按名加载对应文件
Python (pydantic-settings)ENV_FILE=.env.prod / ENV=prod + settings 按 env 读环境变量优先,12-factor 原生
Go viperviper.SetConfigName("dev") / SetEnvPrefix比 goctl 灵活,但要自己接;go-zero 不强制用
写死全局 env 变量var Env="prod" compile 期定反面模式:换环境要重新编译,不可取
go-zero 的标签是「没有魔法开关,靠文件 + 启动参数 + 环境变量」。好处是简单、可预测、契合 12-factor;代价是没有 Spring 那种自动 profile 合并,需要你自己维护多份 yaml(但结构一致、差异清晰,反而更易审查)。