evendeep

实际上,我想要实现一个 deepcopy 库很久了,很多年了,大约在 15 年的时候就渴望手中有这个武器,但是那时候我自己对 reflect 都还没弄明白,写的时候就非常的磕磕绊绊,也就放下了。

后来有一些可堪使用的 deepcopy 库了,就更是放下了。

尽管没有一个已有的能够满足我实践中的要求,但是那又何妨呢,日子还不是要照样过不是吗?

后来写 blog 就把反射有关的知识整理了一下:

当然不是要写一份全面总结,只是针对个人需要而日记罢了。

但这个念头(实现满足自己需要的 deepcopy 库)就挥之不去了。直到上上月初,偶然开启了一个新目录,随手建立了一些骨架后,这项工作终于被不知不觉地开始了执行,然而时间有限,于是就时不时地写一点,搁置几天,回头来又花时间厘清思路,找回状态,又写一点,间或遇到麻烦,动了一点点代码结果到处飘红。

总之一言难尽的吧。

苦力活

evendeep 是个苦力活,因为必须设法去处理全部可能的类型。做到现在,我们基本上可以说办到了,大部分类型都可以被恰当地处理。也就是说基本类型及其复合类型,是可以被处理的,而特殊的类型如 chan,Mutex 等标配或预制类型则能够被恰当地掠过。

在这方面,evendeep 就不像有的三方库,它们要么说我只处理 struct 的 deepcopy,要么什么也不说,等你送个带 chan 的结构进去就 panic 了。这时候,我就很无语,而且还很恼怒,可惜仅此而已了,难不成顺着网线找到对方 ¥%& 他一次。

所以这也是我必须要做一个 deepcopy 的原因。

除此之外,我们不但支持 deepcopy,还同时支持 deepdiff 和 deepequal,这是为了让你不必分别 import,而且即使你可以,也要面对不同的三方库风格、态度各不相同的问题,我们所提供的 deep-series 在处理思路,编程接口等各方面都具备统一性——这当然是我的一贯的建设方式。

值得一提的是,deepdiff 的算法实现,很大层面上照搬了 d4l3k/messagediff 的思路。至于为什么不直接 PR,一是他家这两年没什么 activities 了,其二是 messagediff 有自己的一套编程接口,PR 的话不免会令旧用户被打断。最后再加上我仍有一些其它的特性需要支持,例如我们的 DeepDiff 是允许你以忽略下标顺序的方式比较 slice 的。

DeepCopy 特色

evendeep 提供两个主要的包级函数让你直接开始,一是 DeepCopy,二是 MakeClone。

它们有着不同的场景面向。如果你是在做真正的 deepcopy,有一个确定的 target,那么就应该使用 DeepCopy,否则的话,尤其是当你想获得一个 clone 新副本时,就可以使用 MakeClone。

DeepCopy 的用法

是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func TestDeepCopy(t *testing.T) {
	type AA struct {
		A bool
		B int32
		C string
	}
	type BB struct {
		A int
		B int16
		C *string
	}

	var aa = AA{A: true, B: 16, C: helloString}
	var bb BB
	var ret typ.Any = evendeep.DeepCopy(aa, &bb,
		evendeep.WithIgnoreNames("Shit", "Memo", "Name"))
	t.Logf("ret = %v", ret)
	// ret = &{0 16 &"hello"}
	if *bb.C != helloString {
		t.FailNow()
	}
}

ret 返回值是 interface{},为了在将来能够适应于 1.18+,我们定义了 typ.Any 作为 any 的别名。

DeepCopy 要求第二个参数,也就 target,必须以指针方式提供,也就是说你总是应该去地址。这样从 aa 提取的值才能写入到 bb 中。通常来说,你可以自由地反复取地址,因为 evendeep 的内部会首先脱掉指针的包裹,无论它有多少层。

image-20220501131135531

你可以给 DeepCopy 提供额外的 WithXXX 选项,这是标准的 Options 模式,好像我以前不记得在哪篇 post 中曾经单独描述过。

MakeClone 的用法

而 MakeClone 带有不同的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
func TestMakeClone(t *testing.T) {
	type AA struct {
		A bool
		B int32
		C string
	}

	var aa = AA{A: true, B: 16, C: helloString}
	var ret typ.Any = evendeep.MakeClone(aa)
	var aaCopy = ret.(AA)
	t.Logf("ret = %v", aaCopy)
	// ret = {true 16 hello}
}

由于 MakeClone 语义的原因,它没有任何可选项,你只能得到一个原样照搬的副本。

如果你的结构实现了 Cloneable 接口,它将被用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Cloneable interface represents a cloneable object that supports Clone() method.
//
// The native Clone algorithm of a Cloneable object can be adapted into DeepCopier.
type Cloneable interface {
	// Clone return a pointer to copy of source object.
	// But you can return the copy itself with your will.
	Clone() interface{}
}

// DeepCopyable interface represents a cloneable object that supports DeepCopy() method.
//
// The native DeepCopy algorithm of a DeepCopyable object can be adapted into DeepCopier.
type DeepCopyable interface {
	DeepCopy() interface{}
}

类似的,DeepCopyable 也允许你自己的结构采用你自己的算法。

使用 New()

功能性的类库的标准接口是这样的范式:lib.New().Do(key-param1, key-param2, opts...),或者是 lib.New(key-param1, key-param2, opts...)

如果需要提供关键入参,那么就是 key-param1,key-param2,乃至于更多。然后是任意多的 options 以便向类库的核心结构中灌入工作选项。

使用 New() 的方式区别不大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestNew(t *testing.T) {
	type AA struct {
		A bool
		B int32
		C string
	}
	type BB struct {
		A int
		B int16
		C *string
	}

	var aa = AA{A: true, B: 16, C: helloString}
	var bb BB
  var opts []evendeep.Opt
	var ret typ.Any = evendeep.New(opts...).CopyTo(aa, &bb,
		evendeep.WithIgnoreNames("Shit", "Memo", "Name"))
	t.Logf("ret = %v", ret)
	// ret = &{0 16 &"hello"}
	if *bb.C != helloString {
		t.FailNow()
	}
}

在这里,New(opts…) 和 CopyTo(from, to, opts…) 都接受 opts 的传入,你可以任意选择。

NewDeepCopy 的区别在于,DeepCopy 使用一个全局预置的变量 DefaultCopyController.CopyTo() 来调用核心逻辑,所以 opts 在 DefaultCopyController 中可能会产生叠加效应。而使用 New 总是获取一个新的 copier,你不必担心反复调用时潜在的 opts 叠加问题。

支持的特性

evendeep 支持这些特性:

  • 结构,map,slice,array,标量等的任意相互拷贝
  • 对于非数值类型,例如 chan,将会安全地忽略
  • 不会进入 go 标准库对象的私有结构中(除非你进行了特别的定制)
  • 允许用户类型的私有成员的拷贝
  • 识别循环引用问题
  • 全可定制特性:
    • Value/Type 自定义 Converter 或者 Copier
    • NameConvertRule
    • 结构 Tag 指引的不同策略
    • global source extractor
    • global target setter
  • 两种遍历机制:按下标顺序,按名字对应方式
    • 在 ByName 策略下,NameConvertRule 可以提供名字映射能力,适合于 ORM 场景
  • 充分预制的拷贝和合并策略
  • slice 和 map 默认时启用 merge 机制
  • 同名的 member function 作为源,或者目的地
  • 全局的 StringMarshaller 定义,允许在 deepcopy 时序列化到 json/yaml 等等

不支持泛型

evendeep 没有支持泛型,因为 go118 泛型根本不是为了泛类型而设计的。吐槽这一点,我已经没有兴趣了。需要解释的是,go 泛型支持在未来也不可能提供语言编码支持,令我们能够在泛泛的类型之间进行 deepcopy,正如它也不可能提供 yaml 或者 json 的泛型编码能力一样。咱们这一领域,包括 hedzr/cmdr 的泛类型配置中心在内,是 go 泛型不可能支持的场景。

所以讲到 cmdr 的实现,我其实最满意的反而是 C++ 版本 hedzr/cmdr-cxx,在那边借助于 C++17 的能力,我实现了一个非常舒服的泛类型配置中心,可以严密地配合到与CLI 参数解析能力联动。那才是我觉得正常的泛型。

C# 的泛型能力也充分强,但有点制式化,缺乏灵动性。所以就不鼓吹我的 Cmdr.Core 了。而且另一方面我暂时也没精力去升级 C# 到 5.0,6.0 去。

说到 go 泛型,倒也不是一无是处。如果你在制作一个古典的、单纯的、单一的容器类库,例如 vector 之类,那么 go 泛型还是算好用的。所以,我就把 hedzr/go-ringbuf 升级到 v2 版,这样就能够利用泛型能力从 ringbuffer 的 enqueue/dequeue 操作上消除了 interface{} 装箱拆箱的额外开销。

然而也并没有额外获得特别大的性能提升。这是因为 go 泛型本身也带来了少少的额外开销;此外由于语法限制,我们的编码有时候反而要以分外的笨拙方式来做泛型编码,这也会另外带来不必要的代码开销。最后一点是我们的 ringbuf 的性能本已针对 MPMC 场景做了深层优化,go 代码层面上的优化或损失并不是重点。

最最关键的一个原因,我们的设计目标就是要用反射啊,deepcopy 的功能只能借助 reflect 来做,所以泛型就是多此一举了。

路线图中暂时也不会包含提供类型泛型支持。

使用 WithXXX 选项

已经有一组预定义的选项可以简化或/和控制 evendeep deepcopy 的行为。完整的列表以及功用请查阅代码自动完成列表以及 godoc 文档。在这里我们会拣选少少予以展示。

例如 WithIgnoreNames(names...) 可以提供字段名通配符列表对源进行筛选,这在进行 struct/map 的拷贝时会有用。如果你需要精细的控制,一个方法是在 struct tag 中进行定义,后续的小节中会就此进行提示。

以前文的例子稍作变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func TestWithIgnoreNames(t *testing.T) {
	type AA struct {
		A bool
		B int32
		C string
		D string
	}
	type BB struct {
		A int
		B int16
		C *string
	}

	var aa = AA{A: true, B: 16, C: helloString, D: worldString}
	var bb BB
	var ret typ.Any = evendeep.DeepCopy(aa, &bb,
		evendeep.WithIgnoreNames("C*"),
		evendeep.WithSyncAdvancing(false),
		evendeep.WithByOrdinalStrategyOpt,
	)
	t.Logf("ret = %v, .C = %v", ret, *bb.C)
	// ret = &{0 16 &"world"}
	if *bb.C != worldString {
		t.FailNow()
	}

	var cc BB
	ret = evendeep.DeepCopy(aa, &cc,
		evendeep.WithIgnoreNames("C*"),
		evendeep.WithSyncAdvancing(true),
		evendeep.WithByOrdinalStrategyOpt,
	)
	t.Logf("ret = %v, .C = %v", ret, *cc.C)
	// ret = &{0 16 &""}
	if *cc.C != "" {
		t.FailNow()
	}
}

evendeep.WithByOrdinalStrategyOpt 是默认的,但是为了避免 opts 叠加带来的边际效应,这里显式宣告来保证遍历模式。除此而外,你也可以采用 WithByNameStrategyOpt 的方式来遍历结构成员。

evendeep.WithSyncAdvancing(false) 是默认的,这种方式下,当源的字段被判定忽略之后,相应的目标字段不会向后推进。在示例中,这等效于目标字段的指针保持在 “C”,所以下一个源字段 “D” 被处理为复制到目标的 “C”。

反之,evendeep.WithSyncAdvancing(true) 要求源和目标的字段同步推进,所以源的 “D” 字段没有对应的目标字段进行复制,而目标的“C”字段由于被忽略的原因,所以保持空字符串状态。

evendeep.WithIgnoreNames("C*") 指明所有的 C 开头的源字段名都将被忽略。通配符匹配的算法是标准的文件名通配符模型,至少支持这样的模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
output := IsWildMatch("aa", "aa")
expectTrue(t, output)

output = IsWildMatch("aaaa", "*")
expectTrue(t, output)

output = IsWildMatch("ab", "a?")
expectTrue(t, output)

output = IsWildMatch("adceb", "*a*b")
expectTrue(t, output)

output = IsWildMatch("aa", "a")
expectFalse(t, output)

output = IsWildMatch("mississippi", "m??*ss*?i*pi")
expectFalse(t, output)

output = IsWildMatch("acdcb", "a*c?b")
expectFalse(t, output)

自定义源字段的 Extractor

可以指定一个全局的 Source Field Extractor 选项,当核心逻辑在遍历字段时会尝试使用这个 extractor 来抽取源值。

由于这一功能依赖于你有明确的字段名列表,所以当 extractor 被显式指明时,核心逻辑会采用以目标结构的字段名为导向的方式来做遍历(等同于采用了 WithByNameStrategyOpt )。这样才能有适当的字段名称供给 extractor 用以抽取源值。

反之,正常情况下核心逻辑是以源结构的字段名列表(或者下标顺序)为导向,据此寻找正确的目标字段名并完成复制(或合并)的。

利用 Extractor,我们可以从特殊的数据源中收集和复制数据。下面的例子展示了我们如何从 context.Context 抽出上下文敏感的 Value 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestStructWithSourceExtractor(t *testing.T) {
	c := context.WithValue(context.TODO(), "Data", map[string]typ.Any{
		"A": 12,
	})

	tgt := struct {
		A int
	}{}

	evendeep.DeepCopy(c, &tgt, evendeep.WithSourceValueExtractor(func(name string) typ.Any {
		if m, ok := c.Value("Data").(map[string]typ.Any); ok {
			return m[name]
		}
		return nil
	}))

	if tgt.A != 12 {
		t.FailNow()
	}
}

这种方式需要目标是一个 struct 或者 map,因为只有如此才能拿到目标字段名列表,甚至于对于空 map 的目标来说,此功能也无法有效工作。

自定义 Target Setter

和 Source Field Extractor 相对应的是 Target Field Setter。

下面的示例展示了不同的手法来进行名称转换(而不是使用 NameConvertRule 方式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func TestStructWithTargetSetter(t *testing.T) {
	type srcS struct {
		A int
		B bool
		C string
	}

	src := &srcS{
		A: 5,
		B: true,
		C: "helloString",
	}
	tgt := map[string]typ.Any{
		"Z": "str",
	}

	err := evendeep.New().CopyTo(src, &tgt,
		evendeep.WithTargetValueSetter(func(value *reflect.Value, sourceNames ...string) (err error) {
			if value != nil {
				name := "Mo" + strings.Join(sourceNames, ".")
				tgt[name] = value.Interface()
			}
			return // ErrShouldFallback to call the evendeep standard processing
		}),
	)

	if err != nil || tgt["MoA"] != 5 || tgt["MoB"] != true || tgt["MoC"] != "helloString" || tgt["Z"] != "str" {
		t.Errorf("err: %v, tgt: %v", err, tgt)
		t.FailNow()
	}
}

基于相同的理由,Field Setter 只在 struct 和 map 之间(笛卡尔积)有意义。

如果你需要精细的控制,那么还是应该通过 Struct Tag 中的 NameConvertRule 来达到目的。

如果你想要的是针对特定类型的定制算法的是,另一种可能的途径是 ValueConverter 和 ValueCopier。

自定义一个 Converter

面对特定的源类型和目标类型,evendeep 允许你实现定制的 ValueConverter 或/和 ValueCopier 来控制相应的行为。

1
2
3
4
5
6
7
8
9
10
11
// ValueConverter _
type ValueConverter interface {
	Transform(ctx *ValueConverterContext, source reflect.Value, targetType reflect.Type) (target reflect.Value, err error)
	Match(params *Params, source, target reflect.Type) (ctx *ValueConverterContext, yes bool)
}

// ValueCopier _
type ValueCopier interface {
	CopyTo(ctx *ValueConverterContext, source, target reflect.Value) (err error)
	Match(params *Params, source, target reflect.Type) (ctx *ValueConverterContext, yes bool)
}

ValueConverter 支持从 source 克隆一个新的 target 实例,而 ValueCopier 支持从 source 复制内容到现存的 target 实例。所以通常我们在具体实现时是两者一并做到:可以在 CopyTo 的代码中调用 Transform 的逻辑来简化你的 Converter 代码。

这两种接口分别适合于 MakeClone 和 DeepCopy 的场景,但在核心逻辑里两种接口都会被查阅和应用。

以一个实例来说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type MyType struct {
	I int
}

type MyTypeToStringConverter struct{}

// Uncomment this line if you wanna implment a ValueCopier implementation too: 
// func (c *MyTypeToStringConverter) CopyTo(ctx *eventdeep.ValueConverterContext, source, target reflect.Value) (err error) { return }

func (c *MyTypeToStringConverter) Transform(ctx *eventdeep.ValueConverterContext, source reflect.Value, targetType reflect.Type) (target reflect.Value, err error) {
	if source.IsValid() && targetType.Kind() == reflect.String {
		var str string
		if str, err = eventdeep.FallbackToBuiltinStringMarshalling(source); err == nil {
			target = reflect.ValueOf(str)
		}
	}
	return
}

func (c *MyTypeToStringConverter) Match(params *eventdeep.Params, source, target reflect.Type) (ctx *eventdeep.ValueConverterContext, yes bool) {
	sn, sp := source.Name(), source.PkgPath()
	sk, tk := source.Kind(), target.Kind()
	if yes = sk == reflect.Struct && tk == reflect.String &&
		sn == "MyType" && sp == "github.com/hedzr/eventdeep_test"; yes {
		ctx = &eventdeep.ValueConverterContext{Params: params}
	}
	return
}

func TestExample2(t *testing.T) {
	var myData = MyType{I: 9}
	var dst string
	eventdeep.DeepCopy(myData, &dst, eventdeep.WithValueConverters(&MyTypeToStringConverter{}))
	if dst != `{
  "I": 9
}` {
		t.Fatalf("bad, got %v", dst)
	}
}

Match 函数决定了什么类型将被 MyTypeToStringConverter 所解释,在示例中实现的是从 MyType 到 string 复制时的定制算法。编码完成后的 MyTypeToStringConverter 需要使用 WithValueConverters 来启用。相应地,如果你实现的是 ValueCopier,那么就使用 WithValueCopiers

如果你想将自己的 Converter 持久化登记,那就使用 RegisterDefaultConverters / RegisterDefaultCopiers。它们会登记到 evendeep 的全局注册表,并在任何 CopyTo 时被引用,而无需在每次调用 CopyTo 的时候显示调用 WithValueConverters / WithValueCopiers

1
2
3
4
5
6
7
8
9
10
11
  // a stub call for coverage
	eventdeep.RegisterDefaultCopiers()

	var dst1 string
	eventdeep.RegisterDefaultConverters(&MyTypeToStringConverter{})
	eventdeep.DeepCopy(myData, &dst1)
	if dst1 != `{
  "I": 9
}` {
		t.Fatalf("bad, got %v", dst)
	}

Converters 的内部注册表中,内建支持了这些转换器:

  • fromStringConverter
  • toStringConverter
  • fromFuncConverter
  • toDurationConverter
  • fromDurationConverter
  • toTimeConverter
  • fromTimeConverter
  • fromBytesBufferConverter
  • fromMapConverter

理论上说,我们本应该将一切类型都设计并实现为若干的转换器,例如 struct, slice, array 等等。然而确实存在一些理由使得我们采用了双轨制,即一部分是 copy functors 而一部分是 converters。在 copy functors 中我们实现了 deepcopy 的主要核心逻辑,因为在其中包含了我们已知的一些优化,以及一些应该避免的隐患,以及一些捷径。

当然,最初我们实际上是有一个挺完美的架构的。

只不过嘛,写着写着就开始有一些不那么干净的东西混进去了。一开始大家的愿景都是很高竿的,最后可能还是免不了要妥协,又或者是被迫要弄点脏东西。

暂停了

写到这里,突然就没什么信心了。

所以关于 deepcopy 的介绍就暂停了,以后有心情的话再做其它特性的介绍吧。不如直接看代码去吧。

但是下面还不得不继续罗嗦一下。

DeepDiff 和 DeepEqual

这里就从简了,只给出一个测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
delta, equal := evendeep.DeepDiff([]int{3, 0, 9}, []int{9, 3, 0}, diff.WithSliceOrderedComparison(true))
t.Logf("delta: %v", delta) // ""

delta, equal := evendeep.DeepDiff([]int{3, 0}, []int{9, 3, 0}, diff.WithSliceOrderedComparison(true))
t.Logf("delta: %v", delta) // "added: [0] = 9\n"

delta, equal := evendeep.DeepDiff([]int{3, 0}, []int{9, 3, 0})
t.Logf("delta: %v", delta)
// Outputs:
//   added: [2] = <zero>
//   modified: [0] = 9 (int) (Old: 3)
//   modified: [1] = 3 (int) (Old: <zero>)

没什么别的特别要说的。毕竟它的用途够简单。

DeepEqual 只是 DeepDiff 的一个包装,去掉了 delta 部分,所以示例也都免了。

就这么草草结束吧。

放飞自我

其实我一直在想一个问题,Elon Mask 会不会某一天演变了,不仅是个造车大亨,还是个造火箭大亨,星链大亨,现在更是要演变为媒体舆论工具大亨了,今后是不是就会变成垄断独裁皇帝,某天把他自己放到电脑网络里,造成一个不灭的灵魂,控制这个世界的大多数人。

由于已经半人半机械化了,甚至干脆就完全 NFT 了,所以他就是未来天网的真正起源?

PS:

这篇文章也是放了很长时间,上面谈论 Mask 的言论也随着 Twitter 的最终被收购而变得未可知了起来。

未来什么也说不定。

REFs

Repository:

哦,对了 Again again,evendeep 作为一个 dive-into-anything 库,不再需要一些时日才能放出,现在已经以 pre-release 的方式发布了,先挂个 v0.2 的版本,争取早日固定到 v1 版本。

🔚

留下评论