Files
go-omni/CONFIG.md

10 KiB
Raw Blame History

统一配置加载

v0.20 起,新生成服务的 main.go 调用生成器维护的 config.Load。项目只需要定义业务字段、默认值和校验;日志、HTTP、数据库等配置使用同一流程,配置加载时不打开数据库、监听端口或创建日志文件。

代码默认值 → envDefault(仅填零值)→ 可选配置文件
→ 完整结构体环境覆盖(含别名)→ 框架规范化 → 项目 Validate
→ 初始化日志及项目依赖 → 启动服务

生成项目要维护什么

文件 维护方 用途
main.go 生成器 启动、信号、请求排空、依赖关闭
internal/config/loadGen.go 生成器 调用统一加载入口;纳入 generate --check
internal/config/config.go 项目 Config、Default()、Validate()
internal/config/loader.go 项目 NewLoader(),只有增加文件格式时才需修改
etc/config.yaml 项目 部署配置;可改用 JSON/XML 并传 -config
internal/svc 项目与生成器分文件 初始化依赖、登记清理、AppOptions

比如增加业务的批次数量:在 Config 增加一个字段,在 Validate 加范围校验;需要部署值时改配置文件或环境变量。不修改 main、Load 或 Handler。

// 添加到项目 Config。
BatchSize int `json:"batchSize" envDefault:"100"`

// 添加到项目 Validate。
if c.BatchSize < 1 || c.BatchSize > 1000 {
    return omniConfig.Invalid("batchSize", "must be between 1 and 1000")
}

自动支持 GO_OMNI_BATCH_SIZE。生成服务的空环境值策略为 ignore,0 和 false 是有效覆盖;范围由 Validate 决定。 重新生成保留 config.go、loader.go 和 svc 的手写扩展。

手写项目用法

package main

import (
    "time"
    "git.yuntuo.hk/k2safe/go-omni/config"
)

type Config struct {
    Timeout time.Duration `json:"timeout" envDefault:"3s"`
    Limit   int           `json:"limit" envDefault:"100"`
    Token   string        `json:"-" env:"APP_TOKEN,secret"`
}
func (c Config) Validate() error {
    if c.Timeout <= 0 {
        return config.Invalid("timeout", "must be positive")
    }
    return nil
}

// cfg, err := config.LoadProject("etc/config.yaml", Config{},
//     config.EnvOptions{Prefix: "APP_", Empty: "reject"})

LoadProject 接收结构体默认值,可实现值接收器或指针接收器的 Validate() error。空路径表示不读文件。 DecodeFile 只做文件覆盖,不做 envDefault、环境覆盖或业务 Validate;旧的两参数调用仍有效,可用结构体或结构体指针。低层 ApplyDefaults、OverlayEnv、LoadFile 继续存在。

嵌套校验用 config.AtField("client", err) 加路径前缀。公共原因通过 config.Invalid 提供,原因中不要包含密码、DSN、Token 或输入原值。

JSON / YAML / XML

按文件扩展名选择解析器:.json、.yaml、.yml、.xml,大小写不敏感。所有格式使用 Go 结构体的 json 字段名,无需为 XML 复制一份结构或标签。 以下配置等价:

{"client":{"timeout":"2s"},"enabled":false,"ports":[8080,8081],"optional":null}
client:
  timeout: 2s
enabled: false
ports: [8080, 8081]
optional: null
<config>
  <client><timeout>2s</timeout></client>
  <enabled>false</enabled>
  <ports type="array"><item>8080</item><item>8081</item></ports>
  <optional type="null"/>
</config>

XML 必须使用一个 <config> 根;普通叶节点按目标 Go 类型转换。数组显式 type="array" 加 <item>,不根据重复元素猜测类型。type="array" 空元素表示空数组,type="object" 表示空对象,type="null" 表示 null。any 中的 XML 叶节点默认是字符串,需要动态数字/布尔时标注 type="number" / type="bool";也可用 type="string"。

类型和覆盖规则:

  • 缺失字段保留默认值;显式 0、false、空字符串正常覆盖。
  • null 清空指针、map、slice、interface;普通值字段保持默认值,沿用 Go JSON 解码语义。需要区分 null 时用指针。
  • 数组整体替换;空 slice 保持非 nil;固定长度 Go 数组要求长度一致,避免丢弃多余配置。
  • map 合并键,已提供的键整体覆盖对应值;map 中未提供的键保留。
  • duration 支持 250ms、3s;整数保持精度,动态 JSON 数字使用 json.Number,不先转 float64。
  • 自定义 json.Unmarshaler / encoding.TextUnmarshaler 仍可接入。这些回调是可信项目代码,必须自己保证并发和副作用安全;不要修改共享外部状态。
  • JSON ,string 字段、嵌入字段遵循 JSON 字段选择;未知字段、重复绑定、冲突均拒绝。

内置解析器限制:单文件最多 1 MiB,解析最多 64 层、100000 个节点;绑定和默认值复制也有深度限制。只接受普通文件,不读取设备或命名管道。不支持多文档、重复字段、无效 UTF-8;YAML 拒绝 alias/merge/custom tag,XML 拒绝 DTD/外部实体、命名空间、业务属性和混合内容。XML 仅支持配置所需的子集,不是通用 XML 文档映射器。

这些限制针对启动配置,避免意外超大文件、别名扩展和模糊覆盖。自定义值解码器的内部字段校验由该解码器负责;例如现有错误目录使用其专有解码规则。

环境覆盖

统一入口的 EnvOptions 控制前缀、空值策略、别名和 Lookup。完整 JSON 路径自动转换为 UPPER_SNAKE_CASE:client.timeout → APP_CLIENT_TIMEOUT。

config.EnvOptions{
    Prefix: "GO_OMNI_", Empty: "ignore",
    Aliases: config.ServerEnvAliases("GO_OMNI_", "server"),
}

生成项目同时接受完整的 GO_OMNI_SERVER_HTTP_READ_TIMEOUT 与短别名 GO_OMNI_HTTP_READ_TIMEOUT。完整名存在时优先,即便其空值被 ignore,也不回退别名。不会再二次调用 ApplyEnv 覆盖业务字段。

显式 env:"CUSTOM_NAME" 是绝对名称;env:"-" 跳过子树。json:"-" 只在声明显式 env 名时参与,可让密钥只能从环境加载。secret 标签表达用途,解析错误对所有字段都不打印值。slice/map 的环境值采用 JSON。required 检查字符串非空、指针/集合非 nil;复杂范围由 Validate 负责。详见 完整业务环境覆盖。

显式环境选择日志 stdout/stderr 会清除文件 profile 的 File/Rotation,除非同时提供非空 File 覆盖。该兼容规则由框架处理。

扩展解析器

内置格式直接可用。新增格式只在 internal/config/loader.go 的 NewLoader 注册:

func NewLoader() (*omniConfig.Loader, error) {
    return omniConfig.NewLoader(
        omniConfig.WithDecoder("custom", omniConfig.DecoderFunc(
            func(data []byte) (omniConfig.Document, error) {
                // 演示注册扩展名;实际可在这里调用项目选定的 TOML 等解析库。
                return (omniConfig.JSONDecoder{}).Decode(data)
            },
        )),
    )
}

Decoder 返回 Document:Value 是 map[string]any 根,子值为 string/bool/json.Number/nil/[]any/map[string]any;无自身类型的叶节点可返回 config.Text。Positions 可选,用 client.timeout 或 servers[0].timeout 标记 1-based 行列。不提供位置时仍有文件、字段、原因。

自己的语法错误可返回 &config.Error{Field: "client.timeout", Line: 3, Column: 5, Reason: "expected a duration"}。Reason 必须安全;普通第三方错误仅公开 invalid configuration syntax,内部 cause 通过 errors.Is/As 保留。

注册表属于 Loader 实例,无全局 init 注册;不能覆盖已有扩展名。Loader 可共享,但 Decoder 必须支持并发且不保留/修改输入。解析插件是可信 Go 代码,框架不是插件沙箱。新格式依赖由选择它的项目显式引入,核心不强制引入 TOML 等库。

定位配置错误

config "etc/config.yaml":4:12 field "client.timeout": expected a duration such as 5s
config env "APP_CLIENT_TIMEOUT" field "client.timeout": expected a valid time.Duration

errors.As(err, &diagnostic) 可获取 *config.Error 的 File/Field/Env/Line/Column/Reason;原 DecodeError 名称保留为别名。errors.Is 可判断文件不存在等 I/O 原因。

解析失败显示文件和可获取的行列;Validate 使用 Invalid/AtField 才能定位业务字段。环境覆盖导致的错误优先标环境变量,不错误地归因到文件;数组元素验证也会追溯到覆盖它的集合环境变量。默认值未出现在文件里时没有行列。YAML 语法错误通常只有行号,列显示 1。

不打印底层错误原文或配置值;任意普通 errors.New(...) 校验失败的公开原因是 validation failed,内部原因仍在链中。项目应改用 Invalid 给出安全的原因。第三方子配置校验若没有字段元数据,只定位到对应子树,框架不会从任意错误文案猜字段。

不要把 Unwrap 后的原始错误、整个配置对象写入公开日志/HTTP;自定义解析器与校验器传入的 Reason 由项目负责脱敏。

v0.19 及更早项目迁移

先提交现有代码,再升级匹配的 CLI 与框架。旧项目的 internal/config/config.go 含手写 Load,新生成器会明确阻止 generate/check/dry-run,不会直接覆盖它。

  1. 保留 Config、Default、Validate、业务配置结构及辅助函数。
  2. 删除旧 Load。普通的 defaults/file/env/Validate 流程已由 LoadProject 接管;移除由此不再使用的 import。将自定义默认值和校验分别迁入 Default / Validate,自定义文件格式迁入 NewLoader。
  3. 框架只在 loader.go 不存在时创建 NewLoader;已有项目文件同名时需要保证它提供同签名函数。无需手写 loadGen.go。
  4. 重新生成,执行 generate --check、go test ./...、go build .,再用真实配置验证启动。

保留业务需要的显式 env 标签/规则;如果原 Load 有解密或其他项目特殊步骤,不要直接删除,应先明确其归属,必要时在自定义 Decoder 中处理。生成器不自动搬动或猜测手写行为。api upgrade 仍可预览较早版本的基础设施差异,但已修改的配置必须按上述步骤迁移。

不会移除低层配置 API;本次移除的是新项目里重复手写的加载流程,不删除旧项目的业务文件。

完整可运行例子:examples/configuration。