Files
go-omni/CONFIG.md

10 KiB
Raw Blame History

配置:文件明确声明值的来源

v0.22 起只使用这一条流程:

项目 Default() → 解析选中的一个文件 → 解析字段值中的环境引用
→ 按 Go 类型绑定已提供的字段 → 项目 Validate() → 初始化日志和依赖

没有环境前缀、别名表或隐式二次覆盖。业务只维护 Config、Default()、Validate() 和配置文件。项目 Validate 中继续调用嵌套框架配置的 Validate;生成模板已包含这些调用。验证函数只检查值,不应打开资源。

直接使用

type Config struct {
    Server omniConfig.Config `json:"server"`
    BatchSize int            `json:"batchSize"`
    Enabled bool             `json:"enabled"`
    Password string          `json:"password"`
    Timeout time.Duration    `json:"timeout"`
}
func Default() Config {
    return Config{Server: omniConfig.Default(), BatchSize: 100, Enabled: true,
        Timeout: 3*time.Second}
}
func (c Config) Validate() error {
    if err := c.Server.Validate(); err != nil {
        return omniConfig.AtField("server", err)
    }
    if c.BatchSize < 0 { return omniConfig.Invalid("batchSize", "must not be negative") }
    return nil
}
// 手写项目;生成项目的 Load 已自动调用此入口。
cfg, err := omniConfig.LoadProject("etc/config.yaml", Default())
server:
  address: "${HTTP_ADDR:-127.0.0.1:8080}"
password: "${DB_PASSWORD}"
timeout: "${REQUEST_TIMEOUT:-3s}"
enabled: false

文件未写 batchSize,保留 100;enabled 明确写 false,就使用 false。变量名字完全由文件决定,不需要对应 Go 字段名。配置文件可只列出部署需要变动的字段;不会自动导出/打印包含凭据的完整配置。

引用语法

文件里的值 含义
host: 127.0.0.1 固定值,不读取任何环境变量
password: "${DB_PASSWORD}" 未设置或空字符串报错,不回退到 Default()
port: "${DB_PORT:-3306}" 未设置或为空时使用 3306
sessionToken: "${SESSION_TOKEN:-}" 未设置或为空时使用空字符串,是否合法由目标类型和 Validate 决定
url: "https://${HOST}:${PORT:-443}/v1" 普通字符串中可出现多个引用
literal: "$${DB_PASSWORD}" 得到字面量 ${DB_PASSWORD},不读取变量
literal: "$$cost" $$ 转义一个 $,得到 $cost

变量名遵循 [A-Za-z_][A-Za-z0-9_]*。只支持 ${NAME}、${NAME:-fallback} 和 $$ 转义,不支持 ${NAME-default}、嵌套引用或 shell 表达式。普通单独的 $ 保留。fallback 到第一个 } 结束,可包含冒号、:-、引号等文件格式允许的字符;需要包含 } 的值可使用固定值或环境变量。

解析只扫描文件的已解码字段值,不替换 YAML/JSON/XML 源文本,不扩展字段名,也不扫描 Default() 的字符串。插入的环境值和 fallback 不再展开。密码 DDSFLKJlsf:saf:-123、引号、换行、${OTHER} 均原样保留,不破坏文档结构。字符串不统一 TrimSpace;"0"、"false" 和全空格字符串均属于已提供。

同一变量每次加载最多读取一次;下一次加载会重新读取。没有全局变量值缓存。并发加载各自维护结果,不能把调用者正在修改的默认配置当作共享可变输入。

类型与覆盖

目标 Go 类型决定引用结果的转换:整数、浮点数、布尔值、time.Duration、字符串、指针及已有自定义 JSON/Text 反序列化。数值范围按目标位宽检查,整数不经过 float64。自定义反序列化仍由自定义类型解释文本,不猜测其业务含义。

  • 文件缺失字段保留初始值;显式 0、false、空字符串正常覆盖,不用零值判断字段是否存在。
  • ${MISSING} 失败就报错;需要备用值必须写 ${MISSING:-value}。
  • 字符串编号 "001" 保持 001;绑定到 int 才转整数。环境里的 null 不自动变成空指针,[1,2] 不自动变成数组。
  • 集合直接在文件里声明,可在每个标量元素中使用引用,例如 ports: ["${PORT_A:-8080}", "${PORT_B:-8081}"]。
  • 数组整体替换;空 slice 保持非 nil;固定长度 Go 数组要求长度一致。map 合并键,已提供键的值整体替换。
  • 固定文件 null 沿用 JSON 语义:清空指针/map/slice/interface,普通值字段保持初始值。需要区分 null 时使用指针。
  • any 的引用结果保持字符串,不猜测动态类型。固定 JSON/YAML 数字使用 json.Number 保留精度。
  • 引用成功后的值不会再覆盖一次。旧 GO_OMNI_* 名称只有出现在文件引用中才有作用;不隐式重置日志 file/rotation。

普通固定字符串仍按原有严格绑定规则处理;不会为了支持变量,把所有 "123" 都自动转换成数字。

YAML、JSON、XML 一致

使用 Go 的 json 字段名,无需维护三份 tag。以下三份文件等价:

port: "${PORT:-3306}"
password: "${PASSWORD}"
{"port":"${PORT:-3306}","password":"${PASSWORD}"}
<config><port>${PORT:-3306}</port><password>${PASSWORD}</password></config>

XML 使用 <config> 根,标量占位符写在普通叶元素或 type="string" 元素中,由 Go 字段决定类型;显式 type="number" / type="bool" 表示文件里已经有合法数字/布尔常量。XML 数组用 type="array" 加 <item>;空对象 type="object"、null 用 type="null"。不从重复元素猜测数组。

.api 不经过配置加载器,响应模板 ${code} 不变。如果把响应模板写在 YAML/JSON/XML 配置文件里,须转义:

server:
  response:
    body:
      code: "$${code}"
      msg: "$${message}"
      data: "$${data}"

加载后恢复为 ${code} 等响应占位符,仍由响应编码器处理。

生成项目与检查

文件 维护方
main.go、internal/config/loadGen.go 生成器维护,纳入 generate --check
internal/config/config.go 项目维护 Config、Default、Validate
etc/config.yaml 等选定文件 项目维护,重新生成不覆盖
internal/config/loader.go 初次生成即可使用,仅添加自定义格式时才修改
go run . -config etc/config.test.yaml -check-config
go run . -config etc/config.local.yaml
go build .
go-omni api generate --check

-check-config 使用与启动完全相同的 Load,通过时退出 0、失败时非零;不打印配置,不创建日志文件、数据库、后台组件或 HTTP 监听。它只校验配置,不检测真实数据库连接或证书握手。

-config 只选一个文件,不自动叠加 local/test/default。指定文件不存在时报错;-config '' 只使用 Default() 后校验,不读取任何环境变量,也不寻找其他文件。示例:examples/configuration。

安全诊断与限制

config "etc/config.yaml":7:9 env "DB_PORT" field "database.port": expected a valid int

*config.Error 包含 File/Field/Env/Line/Column/Reason。引用错误同时保留文件与环境变量名;组合引用失败可列出多个名字。行列定位原文件字段,不把环境值伪装成源文件文本。无法取得位置时仍提供可用的文件/字段信息。

用 config.Invalid(field, publicReason) 或 config.AtField(prefix, err) 报业务校验错误。未知字段给出唯一且接近的已声明字段建议,不自动修改。错误默认不打印原值;自定义 Reason 不应拼入秘密值。原始 cause 仍支持 errors.Is/As,不要把 Unwrap 后的错误或整个配置写到日志。

文件最多 1 MiB;单个环境值/展开字段最多 1 MiB,展开字符串合计/变量缓存分别最多 4 MiB;解析/展开限制 64 层、100000 个节点与引用计数。拒绝无效 UTF-8、多文档、重复字段、未知字段、非普通文件;YAML 不接受 alias/merge/custom tag,XML 不接受 DTD/外部实体。不执行 shell,不自动加载 .env。

可选解析器与测试来源

内置格式无需项目手写加载器。需要额外格式时在已有 NewLoader 注册 Decoder,后续仍自动经过同一引用/绑定/校验流程:

func NewLoader() (*omniConfig.Loader, error) {
    return omniConfig.NewLoader(
        omniConfig.WithDecoder("custom", omniConfig.JSONDecoder{}),
    )
}

Decoder 返回 Document(map/string/bool/json.Number/nil/[]any,XML 类标量可用 Text),可提供 Positions。实例注册无全局 init;插件必须可并发调用、不修改共享输入,并且是可信 Go 代码。

测试或嵌入宿主可以用 WithLookupEnv(func(string)(string,bool)) 替换引用查找函数。它仍然只读取文件明确引用的名称,不启用任何自动覆盖。普通业务不需要这个选项。

v0.22 迁移:旧模式已移除

这是不兼容调整,不保留两套加载逻辑:

  • 删除 EnvOptions、LoadFromEnv、ApplyEnv、OverlayEnv、ApplyDefaults、ServerEnvAliases 和 env/envDefault 标签处理;检出遗留标签会提示迁移。
  • 删除 database.ConfigFromEnv / database.ApplyEnv;数据库配置作为项目 Config 的字段绑定。DSN 字段可在文件里使用 dsn: "${DB_DSN}",序列化 database.Config 不输出 DSN 原值。
  • LoadProject(path, defaults[, loader]) 不再接收 EnvOptions;LoadFile(path)、NewReloader(path, logger) 不再接收前缀。
  • omni.LoadConfig(path) 的参数现在是文件路径;空路径只用框架默认值。

迁移顺序:

  1. 将 envDefault 默认值搬入 Default(),移除 env/envDefault 标签。需要文件绑定的旧 json:"-" 密钥字段改成明确 json 名称;不打印配置对象。
  2. 按实际部署需要,在选定文件写环境引用。固定值保持固定,不按原前缀猜测。原先依赖环境切换日志输出的项目,应在文件明确配置 file/output/rotation。
  3. 配置文件中的其他 ${...} 模板改成 $${...} 字面量;.api 无需修改。
  4. 手写项目改为 LoadProject。生成项目保留 Config/Default/Validate/NewLoader,删除旧手写 Load 后重新生成;main/loadGen 由生成器更新,业务配置文件不会被静默改写。
  5. 升级匹配的 CLI 与框架,执行 generate、generate --check、go test、go build 和 -check-config,再使用真实部署配置启动。

低层 DecodeFile(path, defaults[, loader]) 也解析显式引用,但不调用 Validate;默认值和原始 Document 都不会因失败而被部分修改。日志 Reloader 仍只允许 log.level 变化,其他静态字段变更要求重启。