clone是深拷贝还是浅拷贝_go-clone:深拷贝 Go 数据结构
继续闲来写写码,这次来介绍一下半年多以前写的一个库,最近工作中发现真的有用,还是值得推荐一下的。
背景
这个库是 github.com/huandu/go-clone,主要用途是对任意的 Go 结构进行深拷贝,创造一个内容完全相同的副本,得到的值可通过 reflect.DeepEqual 检查。
这个功能看起来挺常用的,不过很奇怪在 Go 世界里面可用的实现却很少,在动手实现之前我调查了几个类似的库或者可用来做深拷贝:
- encoding/gob 或 encoding/json:先将数据结构进行编码(gob.Encoder 或 json.Marshal),得到
[]byte
之后再解码(gob.Decoder 或 json.Unmarshal)。这种做法的好处是简单粗暴,基本上能够应对大部分的需求,缺点则是性能极低,且有各种限制,比如无法处理递归指针、无法处理interface
类型数据,特别是 JSON,会丢失缺少大部分数据类型甚至精度。 - github.com/jinzhu/copier 或 github.com/ulule/deepcopier:这两个库都实现了基本的
struct
拷贝能力,不过缺乏递归指针的处理,也不能作为通用的深拷贝来使用。
如果大家有看到其他类似功能的库,欢迎留言,我会去研究学习一下。当前所看到的几个库都不太能满足需求。
实现思路
要实现深拷贝函数 Clone(v interface{}) interface{}
,其基本思路很简单:
- 首先通过函数
val := reflect.ValueOf(v)
拿到v
的反射值; - 根据
val.Kind()
区分各种类型,主要分两种:一种是 scala 类型,即数值类型,包括各种整型、浮点、虚数、字符串等,直接返回原值即可;一种是复杂类型,每种类型用对应的反射方法来创建,包括reflect.New
/reflect.MakeMap
/reflect.MakeSlice
/reflect.MakeChan
等方法; - 通过各种反射方法来将新申请
val.Set*
方法将新值设置到新申请的变量里面。
这里面比较麻烦的是处理 struct
,为了深拷贝结构,必须首先通过 val.NumField()
得到 struct field 个数,然后用循环不断的将 val.Field(i)
的值拷贝到新申请的变量对应字段里面去,这里递归调用深拷贝方法即可。
思路看起来很简单,似乎都是些体力活,但做了之后就会发现有一些特殊情况还得多加小心,真要实现好不容易。
处理递归数据
当我们使用循环链表的时候就会遇到递归数据。一个首尾相连的链表,如果一直跟着指针深拷贝所有数据,那么深拷贝函数一定会陷入死循环而无法退出。
下面是一个例子。
type ListNode struct {Data intNext *ListNode
}
node1 := &ListNode{Data: 1,
}
node2 := &ListNode{Data: 2,
}
node3 := &ListNode{Data: 3,
}
node1.Next = node2
node2.Next = node3
node3.Next = node1
其中 node1 -> node2 -> node3 -> node1 -> ...
形成了一个循环链表。
如果直接按照一般思路来实现 Clone
,这个函数会因为不断的深度遍历 Next *ListNode
而陷入死循环,永远无法返回。
为了解决这个问题,应该使用经典的有向图检查环路的方法来实现,需要记下那些会产生循环的类型的访问记录,下次再访问到同样的数据时直接返回之前记录的结果即可打破循环。
可能会循环的类型其实不多,只有map
、slice
和指针总共三种。 这个事实可能会有点违反直觉:struct
和 interface
都不会造成循环?这还真不会。
我们无法仅通过 struct
嵌套来构造出一个循环结构,这是无法通过编译器检查的。我们也无法通过 struct
+ interface
构造出循环结构,考虑以下代码:
type T struct {Loop interface{}
}// 无法不使用 map、slice 和指针构造出循环结构。
t := T{}
t.Loop = t
t.Loop = tfmt.Println(t == t.Loop.(T)) // false// t 实际内容是:
// t == T{
// Loop: T{
// Loop: T{},
// },
// }
可以看到,在 Go 里面并不能把 interface
当做一种万能指针,当我们将一个结构 T
赋值给 interface{}
时候,Go 内部会将 T
的内容拷贝一份再赋值,而不是「引用」T
的原值。
在实现环路检查时还会遇到一个问题:虽然检查循环的关键是发现访问了一个访问过的值,但问题是怎么才知道两个值相等呢?我们总不能用 reflect.DeepEqual
来检查吧,那就太浪费性能了。我们也不能使用 map[interface{}]struct{}
来判断,虽然 Go 允许用 interface{}
作为 map
的 KeyType
,但 Go 编译器和运行时都不允许将 map
类型作为 KeyType
,而可循环类型包含 map
,所以还得找其他办法
m := map[interface{}]bool{}
key := map[int]string{}
m[key] = true // 可以被编译,但是运行时会 panic。
容易想到,同样的问题 reflect.DeepEqual
也会遇到,那么直接去看一下官方实现就能得到答案。
// During deepValueEqual, must keep track of checks that are
// in progress. The comparison algorithm assumes that all
// checks in progress are true when it reencounters them.
// Visited comparisons are stored in a map indexed by visit.
type visit struct {a1 unsafe.Pointera2 unsafe.Pointertyp Type
}func deepValueEqual(v1, v2 Value, visited map[visit]bool, depth int) bool {// 略……
}
继续看代码可以发现,官方使用的是 reflect.Value
的 Pointer
方法来得到 unsafe.Pointer
,恰好 map
、slice
和指针都可以调用这个方法,因此我们可以用类似手法实现。需要注意的是,reflect.DeepEqual
需要判断 v1
/ v2
是否不同,所以在 visit
里面同时记录了两个指针,但我们在 Clone
的时候只需要知道变量是否已经遍历过,所以只需要一个指针就可以。
同时,我们也不需要使用 unsafe.Pointer
来记录指针,直接使用 uintptr
即可。这是因为在 Clone
结束前,新申请的变量一定能被当前 goroutine 的 stack 访问到,不会被 GC。当前 Go 的 GC 也不支持内存移动,可预见的将来也不会支持这种能力,所以无需多此一举用 unsafe.Pointer
平添 GC 压力。
还有一个细节必须注意:通过反射拿到的 slice
内部指针只是 slice
第一个元素的地址,不足以区分不同长度的 slice
,这会造成误判。 reflect.DeepEqual
更注重的是数据「相等」,而不是精确「相同」,不做区分也没事,但 Clone
时候则必须区分同一个数组的不同 slice
的问题。
slice := []int{1, 2, 3, 4, 5}
s1 := slice[:2]
s2 := slice[:5]p1 := reflect.ValueOf(s1).Pointer()
p2 := reflect.ValueOf(s2).Pointer()fmt.Println(p1 == p2) // true
综上,最后采用如下结构来记录访问过的值。
type visit struct {p uintptrextra intt reflect.Type
}type visitMap map[visit]reflect.Value
这个 extra
里面存储的是 slice
的长度。
在记录时也需要注意,必须先将新数据放入 visitMap
然后再深度遍历和填充数值才行,否则依然会死循环。以指针为例,与 visitMap
相关的实现代码如下:
func clonePtr(v reflect.Value, visited visitMap) reflect.Value {t := v.Type()if visited != nil {visit := visit{p: v.Pointer(),t: t,}if val, ok := visited[visit]; ok {return val}}elemType := t.Elem()nv := reflect.New(elemType)if visited != nil {visit := visit{p: v.Pointer(),t: t,}visited[visit] = nv}// 省略填充 nv 内容的过程……return nv
}
最后,由于记录 visitMap
会有额外内存和性能损耗,而绝大多数数据结构并不会包含任何循环结构,所以 clone.Clone
默认不做任何循环检查,专门提供的 clone.Slowly
则负责解决这种复杂问题。
小结
以上内容已经可以帮助我们了解如何实现一个简单可用的深拷贝的工具库了, 这篇文章就暂时到此为止。
其实 github.com/huandu/go-clone 已经实现的功能远不止如此,还有不少硬核内容值得在未来继续分享。例如,如何深拷贝 struct
里面未导出的私有数据、如何极致优化深拷贝性能、如何实现 immutable struct 数据等。这些内容以后再分享吧。
clone是深拷贝还是浅拷贝_go-clone:深拷贝 Go 数据结构相关推荐
- 详解深拷贝和浅拷贝以及如何深拷贝
深拷贝和浅拷贝 一.如何区分深拷贝和浅拷贝 二.举例加深理解深拷贝和浅拷贝 三.图文理解 四.哪些方法是浅拷贝,如何进行深拷贝 一.如何区分深拷贝和浅拷贝 内在的区别:浅拷贝就是简单的把指向别人的值的 ...
- Javascript中的深拷贝和浅拷贝
文章目录 JavaScript中的变量类型 深拷贝和浅拷贝的理解 深拷贝和浅拷贝的实现方式 为什么需要深拷贝和浅拷贝 JavaScript中的变量类型 (1).基本类型 JavaScript中的基本类 ...
- PHP中对象的深拷贝与浅拷贝
2019独角兽企业重金招聘Python工程师标准>>> 最近写了一个php脚本,在脚本中使用到了SplPriorityQueue,会用到两次 因此写了如下: $res=$tmp_re ...
- Java基础—复制之深拷贝与浅拷贝
目录 一.浅拷贝(Shallow Copy) 二.深拷贝(Deep Copy) Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去 ...
- 详谈Javascript中的深拷贝和浅拷贝
数据复制是我们编程中经常会使用到的技术,对于普通数值数据来说,复制很简单,但是对于复杂类型比如对象的复制,就会有很多需要考虑的东西,比如我们经常说到的深拷贝和浅拷贝. 浅拷贝 复制的对象和原始对象属性 ...
- python list 深复制_Python中list的复制及深拷贝与浅拷贝探究
在Python中,经常要对一个list进行复制.对于复制,自然的就有深拷贝与浅拷贝问题.深拷贝与浅拷贝的区别在于,当从原本的list复制出的list之后,修改其中的任意一个是否会对另一个造成影响,即这 ...
- Python中list的复制及深拷贝与浅拷贝探究
在Python中,经常要对一个list进行复制.对于复制,自然的就有深拷贝与浅拷贝问题.深拷贝与浅拷贝的区别在于,当从原本的list复制出的list之后,修改其中的任意一个是否会对另一个造成影响,即这 ...
- Python基础教程:list深拷贝和浅拷贝
在Python中,经常要对一个list进行复制.对于复制,自然的就有深拷贝与浅拷贝问题.深拷贝与浅拷贝的区别在于,当从原本的list复制出的list之后,修改其中的任意一个是否会对另一个造成影响,即这 ...
- JavaScript 深拷贝与浅拷贝
目录 深拷贝与浅拷贝 简单区分 js数据类型 基本数据类型 引用数据类型 两者存储上的区别 js实现深拷贝 深拷贝与浅拷贝 简单区分 深拷贝与浅拷贝的简单区分: 假设B复制了A,当修改A时,看B是否跟 ...
最新文章
- noip2016 小结(ac两题+学习总结)
- steam怎么看邮箱绑定的账号_lol手游appleid怎么绑定拳头账号 英雄联盟手游账号绑定方法_英雄联盟手游...
- ASP.NET-自定义HttpModule与HttpHandler
- Exynos4412 裸机开发 —— IIC总线
- php spss,spss新手入门
- oracle in 索引_Oracle 性能优化总结
- 用 X 光检测新冠肺炎?也许孪生网络+迁移学习是更好的选择!
- JBox2D手机游戏引擎介绍(附jbox2d官网网址)
- 防止 MySQL-PHP 网站被玩弄的方法
- 【高速PCB电路设计】1.高速PCB设计概述
- 龙芯2h芯片不能进入pmon_基于龙芯2F架构的PMON分析与优化
- access如何设置定期报表汇总_access 2007创建分组报表或汇总报表
- Spark课程设计——电影推荐系统
- java分数计算_分数计算java
- 喜欢吃鱼的朋友一定要存哦
- Python小技 不到100行代码制作各种证件照
- 【计算机毕业设计】基于微信小程序的图书馆座位预约系统
- 常用函数式接口及Stream流
- MPC系列:Beaver三元组和BMR协议
- POI删除Excel中数据有效性
热门文章
- antv图例出现分页_带图例、文本的饼图
- mysql 分表全局id_MySQL分庫分表環境下全局ID生成方案
- Python函数16道入门练习题
- python3函数中lambda/filter/map/reduce的用法
- Python:if判断与while、for循环语句
- Python进阶: Decorator 装饰器你太美
- 为什么ubuntu64位下C语言for循环不能超过2147483647次?(size_t )
- VMware ubuntu 虚拟机只剩下lo回环网卡,上不了网(无法上网、无法连接网络)(环回)
- win10下输入法突然变成繁体了怎么设置回来?(繁體)(快捷鍵:ctrl + shift +f)
- tensorflow在训练的时候权重是nan,如何解决