Files
go-omni/COPY.md

10 KiB
Raw Blame History

可选拷贝工具 copyx

v0.19 用普通 Go 调用取代 JSON 结构映射生成。copyx 只依赖标准库,不需要注册到 App,不执行 JSON 往返,也不会自动暴露数据库模型。普通 Logic 直接使用 .api 生成的 DTO;只有存在独立业务类型或需要隔离可变数据时才用它。

完整可运行代码:examples/copying。在框架根目录运行:

go run ./examples/copying
go test -race ./copyx ./examples/copying

最简单的调用

import "git.yuntuo.hk/k2safe/go-omni/copyx"

type Request struct { Name string; Enabled bool }
type Input struct { Name string; Enabled bool }

req := Request{Name: "sample", Enabled: false}
var input Input
if err := copyx.Copy(&input, req); err != nil { return err }

同名、同类型字段直接赋值。默认不会跳过 false、0、空字符串。目标字段没有来源时报错;多余源字段默认忽略,使用 WithStrictFields() 可要求源字段也全部使用或显式忽略。

默认是浅拷贝:同类型指针、map、slice 共享引用,符合普通 Go 赋值。JSON tag 不参与字段匹配。不会自动调用方法、SQL Scanner/Valuer,或把 int32、int64、string、命名标量互相强转。

新字段:普通接口只改 .api 和使用该字段的 Logic。独立 DTO/业务类型都需要此字段时,两边各声明一次;同名同类型不增加拷贝配置。检查在运行时发生,实际类型对必须有测试,不能把运行时工具当作旧生成器的编译期字段覆盖检查。

深拷贝与构造器

snapshot, err := copyx.Clone(source) // 保持源的静态类型,始终深拷贝
if err != nil { return err }

copier, err := copyx.New(copyx.WithDeepCopy(), copyx.WithStrictFields())
if err != nil { return err }
var result Output
if err := copier.Copy(&result, source); err != nil { return err }

Copy 的目标必须是非 nil 指针;Clone[T] 返回 (T, error)。重复执行时在初始化阶段创建 Copier 并复用;配置之后不可变,每次调用独立持有遍历状态,可以并发使用。源、目标的并发读写仍由调用方同步,自定义函数也须支持并发。

深拷贝会分配指针、map、slice,递归处理结构、数组、接口中的具体值。nil 集合与非 nil 空集合保持区别;slice 结果容量为长度,不复制长度之外的隐藏元素。普通循环和重复指针可处理。循环图应保留根指针,使用 Clone(sourcePointer) 或 Copy(&targetPointer, sourcePointer);将循环指针根压成值结构会明确报错。

它保证结果不引用输入中的可变对象,不保证保留所有底层内存布局关系:不同重叠 slice 视图会分别复制,内部指针指向某个切片元素的关系也不重建。字符串和零尺寸值可安全共享;time.Time 按 Go 不可变值使用(Location 不做独立复制)。不要通过 unsafe 或 uintptr 隐藏可变引用后要求工具识别。

字段差异与自定义转换

type Input struct { Name string; UpdatedAt time.Time; InternalID string }
type View struct { DisplayName string; UpdatedAt string; LocalNote string }

copier, err := copyx.New(
    copyx.WithDeepCopy(),
    copyx.WithStrictFields(),
    copyx.WithFieldNames[Input, View](map[string]string{"Name": "DisplayName"}),
    copyx.WithIgnoreSource[Input, View]("InternalID"),
    copyx.WithIgnoreTarget[Input, View]("LocalNote"),
    copyx.WithConverter(func(v time.Time) (string, error) {
        return v.UTC().Format(time.RFC3339), nil
    }),
)
if err != nil { return err }
out := View{LocalNote: "preserve"}
if err := copier.Copy(&out, input); err != nil { return err }

字段规则的类型参数必须是结构类型,字段名使用导出的 Go 字段名。构造时检查不存在的字段、重名及冲突。不支持字符串路径表达式;复杂嵌套差异为该嵌套类型注册 converter,或直接写 Go。

WithIgnoreSource 只是声明该源字段不参与使用;目标同名字段若仍需保留,还需 WithIgnoreTarget。忽略目标会保留原目标值,Clone 中则为零值。深拷贝时如果保留值仍与源共享可变引用,也会拒绝。

转换器的签名是 func(S) (T, error),按精确类型对匹配,不能重复注册。金额、编号、日期格式由项目明确转换。需要逐字段不同规则时,给整个源/目标结构注册函数;函数返回独立的新目标。无需另一份映射配置或全局注册。

精确 converter 优先于 nil/零值策略,能收到 typed nil;例如 func(*Money) (Money, error) 可自行决定 nil 是否允许。绑定到 any 字段的 converter 能接收 nil interface,但顶层未带类型的 nil 没有精确类型可匹配。浅拷贝中已经直接共享的同类型容器不会继续逐项转换其内部;需要遍历内部时选择深拷贝,或为该容器类型注册 converter。

深拷贝不能原样返回可变引用

copier, err := copyx.New(
    copyx.WithDeepCopy(),
    copyx.WithConverter(func(v []string) ([]string, error) {
        return v, nil // 会返回 ErrSharedReference
    }),
)

检查覆盖完整源图与转换结果,包括新父对象引用旧子对象、原 map、重叠子切片、source[:0] 的可写容量、指向源数组元素的指针,以及未导出字段中的引用。会检查每个 converter 的结果和最终目标,不允许静默降级为浅拷贝。

返回相同字符串、整数并不会报错,它们不是可变引用。正确切片 converter 可以写:

copyx.WithConverter(func(v []string) ([]string, error) {
    if v == nil { return nil, nil }
    out := make([]string, len(v))
    copy(out, v)
    return out, nil
})

第三方 Decimal/自定义 Money 如果含未导出的指针,默认深拷贝拒绝;注册该类型的 converter,使用它自己的构造/Clone API 创建独立值,避免转成 float64 损失精度。框架不内置某一金额库的规则。无法独立复制的类型直接返回错误。

nil、零值和错误

选项 行为
默认 NilError nil → 可空目标保留 nil;nil → 非可空目标报 ErrNil
WithNilPolicy(NilZero) 显式允许 nil → 零值;可空目标仍为 nil
WithNilPolicy(NilSkip) nil 保留已有目标;新集合元素没有旧目标,保留元素零值
WithSkipZero() 显式跳过 Go 零值,包括 false、0、"";非 nil 空集合不是 Go 零值

所有 nil 策略都不把不兼容类型自动变成兼容类型。非 nil 指针与值之间可以按对应元素转换;nil 到非可空目标仍需选择策略。局部更新不能默认使用 NilZero/SkipZero,它们可能改变清空字段或布尔值的业务语义。

工具只能复制绑定后的 Go 值:如果输入类型已经将“缺失”和“JSON null”合并为 nil,无法恢复两者区别。需要三态时,应先设计包含 presence 信息的类型和绑定逻辑。

赋值使用临时结果,成功后才替换目标;转换失败不会交付部分结果。*copyx.Error 包含 Go 字段/索引路径,errors.Is/As 保留内部错误链,工具不主动在错误中加入源值。converter 自己返回的错误文本仍由项目负责。

converter 必须只读输入;其外部副作用、输入修改无法回滚,panic 原样传播。框架只检查与本次输入的共享引用,无法证明 converter 未返回外部全局缓存的引用。它是受信 Go 代码扩展,不是可运行不可信代码的沙箱。

支持范围与资源限制

  • 支持结构体、指针、interface、slice、同长度 array、map。map key 转换后冲突或不可比较则报错。
  • 导出匿名结构字段可按提升后的无歧义名称访问;源嵌入指针为 nil 且需要其字段时明确报错。跨类型目标包含私有字段时拒绝;同类型私有纯值可作为整体保留。
  • 不克隆文件、数据库连接、网络连接、锁、原子状态。已知标准库资源/同步结构按值复制也拒绝;深拷贝中的非 nil func、chan、unsafe.Pointer 拒绝。其他封装资源类型应由项目排除,工具不能识别所有自定义句柄。
  • 单次遍历深度上限 256、节点上限 200,000;内置分配的估算预算为 64 MiB,超限返回 ErrLimit。预算不是进程堆内存硬限制,reflect 元数据和 converter 自行分配不计入。大对象流式读写应使用 IO/存储接口。
  • 只缓存不可变类型元数据,不缓存请求值、用户 converter 或实例策略。没有 unsafe 写入、JSON 往返、隐式数值截断或方法调用。
  • 这是运行时反射工具,会比直接 Go 赋值多检查与分配。热点应复用 Copier,并用真实数据测量;性能记录见 本版验收。

从旧 mapping 迁移

v0.19 彻底移除映射配置加载、Go 类型加载、映射代码生成及 pointerResult 等附属选项。公共 .api 字段组合、scalar、中间件工厂、Repository 开关继续存在。

  1. 先检查每个转换是否必要。普通 Logic 可直接使用生成 DTO;独立核心保留自己的类型。
  2. 将 mapping.Xxx(...) 调用改为普通 Go 或小型 copyx helper。字段改名、nil、金额规则写在该 helper 同一处。指针返回可直接 var out *Response; err := copier.Copy(&out, input)。
  3. 用真实类型对测试缺失/null/零值/空数组、字段覆盖、金额精度和错误链。需要源字段新增时提醒遗漏,启用 WithStrictFields() 并测试。
  4. 从 omni.gen.json 删除 mappings / mappingFiles,删除无其他用途的映射 JSON 文件。
  5. 确认已无调用,再删除 internal/mapping/mappingsGen.go。如之前手改过文件,先迁出手写内容。生成器遇到该旧文件会阻止生成并提示迁移,不会静默删除;其余业务文件照常保留。
  6. 升级框架与 CLI 到同版,执行 generate、generate --check、go test ./...;多个服务再执行 workspace generate/check。新 manifest 自动去掉已移除文件的所有权记录。

历史发布记录保留旧能力说明,仅用于理解旧版本,不代表 v0.19 继续支持。