Golang Go语言中向繁琐的赋值代码说不。deepcopy.Copy 深度拷贝来了
Golang Go语言中向繁琐的赋值代码说不。deepcopy.Copy 深度拷贝来了
出发点
一次帮同事 review 代码,想在 go 里面找一个支持深度 Copy 的库,github 上少得可怜。最后找到 json marshal 加 unmarshal 的方式,但是这种方式有两个缺点,第 1 marshal 一次 reflect,unmarshal 一次 reflect,有两次 reflect 的过程,效率会垫底。第 2,不支持过滤条件,这点硬伤,改不了。特对这两点问题,所以想撸个改进版本(更快,更可控)。
项目地址
https://github.com/antlabs/deepcopy
作用
deepcopy.Copy 主要用于两个类型间的深度拷贝[从零实现]
feature
- 支持异构结构体拷贝, dst 和 src 可以是不同的类型,会拷贝 dst 和 src 交集的部分
- 多类型支持 struct/map/slice/array/int...int64/uint...uint64/ 等等
- 性能相比 json 序列化和反序列化的做法,拥有更快的执行速度
- 可以控制拷贝结构体层次
- 可以通过 tag 控制感兴趣的字段
内容
Installation
go get github.com/antlabs/deepcopy
Quick start
package main
import (
“fmt”
“github.com/antlabs/deepcopy”
)
type dst struct {
ID int
Result string
}
type src struct{
ID int
Text string
}
func main() {
d, s := dst{}, src{ID:3}
deepcopy.Copy(&d, &s).Do()
fmt.Printf("%#v\n", d)
}
max copy depth
如果 src 的结构体嵌套了两套,MaxDepth 可以控制只拷贝一层
deepcopy.Copy(&dst{}, &src{}).MaxDepth(1).Do()
copy only the specified tag
只拷贝结构体里面有 copy tag 的字段,比如下面只会拷贝 ID 成员
package main
import (
“fmt”
"github.com/antlabs/deepcopy"
)
type dst struct {
ID int copy:"ID"
Result string
}
type src struct {
ID int copy:"ID"
Result string
}
func main() {
d := dst{}
s := src{ID: 3, Result: “use tag”}
deepcopy.Copy(&d, &s).RegisterTagName("copy").Do()
fmt.Printf("%#v\n", d)
}
copy slice
package main
import (
“fmt”
"github.com/antlabs/deepcopy"
)
func main() {
i := []int{1, 2, 3, 4, 5, 6}
var o []int
deepcopy.Copy(&o, &i).Do()
fmt.Printf("%#v\n", o)
}
copy map
package main
import (
“fmt”
"github.com/antlabs/deepcopy"
)
func main() {
i := map[string]int{
“cat”: 100,
“head”: 10,
“tr”: 3,
“tail”: 44,
}
var o map[string]int
deepcopy.Copy(&o, &i).Do()
fmt.Printf("%#v\n", o)
}
性能
从零实现的 deepcopy 相比 json 序列化与反序列化方式拥有更好的性能
goos: linux
goarch: amd64
pkg: github.com/antlabs/deepcopy
Benchmark_MiniCopy-12 243212 4987 ns/op
Benchmark_DeepCopy-12 273775 4781 ns/op
PASS
ok github.com/antlabs/deepcopy 4.496s
更多关于Golang Go语言中向繁琐的赋值代码说不。deepcopy.Copy 深度拷贝来了的实战教程也可以访问 https://www.itying.com/category-94-b0.html
咋看着提升不是很明显
更多关于Golang Go语言中向繁琐的赋值代码说不。deepcopy.Copy 深度拷贝来了的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
试着对比了一下 jsoniter
要不要试着做一下缓存?
Benchmark_MiniCopy
Benchmark_MiniCopy-12 223624 5366 ns/op
Benchmark_DeepCopy
Benchmark_DeepCopy-12 321472 3703 ns/op
Benchmark_jsoniter
Benchmark_jsoniter-12 471108 2422 ns/op
PASS
图片贴不出来,这样看一下吧
jsoniter 里面也用的 reflect API ?晚上我加下缓存优化下。
jsoniter 第一次会反射,但是反射出来的结果会缓存
其实这样代码生成的方式也挺不错的,牺牲掉一点维护性也是可以接受的
https://github.com/globusdigital/deep-copy
当然,golang 没有像 BeanCopier 这样的神器的确是比较可惜了…
可否把你的 benckmark 代码发下。我优化下,再看下性能。
标准库里面的代码做了缓存,所有第一个版本只领先了 18%-30%。如果用同样的思路优化,领先的会更多。
毕竟序列化,反序列化的方式深度拷贝要两次 reflect 。
谢了。
go 原生赋值就是深拷贝啊,你这个是标题党吧。
你这个最多算是异构赋值
slice, map 可以深度拷贝?
结构体里面套指针,套 interface{},套 slice,套 map,不可以深度拷贝。
和 jinzhu/copier 对比下?
好,会压测下,结果到时候通知。
不支持多重指针, 比如一个 *int 字段往 **int 字段赋值, 就会报错, 如果用 json 包可以处理
这种情况 jinzhu/copier 也不支持, 但 github.com/petersunbag/coven 支持, 而且更快? 希望楼主加入支持后, 再压测一下
我只有一个疑问:
支持 tag 是不是多余了?我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗??
我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……
hi rrfeng 。不加 tag 可以直接拷贝的。所有 ->“我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗??”,所以,不 tag,不需要写 copy 方法会更更爽快。。。
从 ->“我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……” ,这里说了 if 的情况,所以 else 也是有点用的,比如都是自己的包,刚好要过滤几个字段。。。
链式调用重构一下?不然都是 Do<br>deepcopy.RegisterTagName("copy").Copy(&d, &s)<br>
ok, 我思考下。
goos: darwin
goarch: amd64
pkg: deepcopy
Benchmark_MiniCopy
Benchmark_MiniCopy-12 182653 5688 ns/op
Benchmark_DeepCopy
Benchmark_DeepCopy-12 313747 3953 ns/op
Benchmark_jsoniter
Benchmark_jsoniter-12 495062 2476 ns/op
Benchmark_copier
Benchmark_copier-12 7714009 152 ns/op
Benchmark_coven
Benchmark_coven-12 7289439 160 ns/op
PASS
试了一下刚才看到的两个库,效果非常好
测试错了吧,把代码贴到 V2EX 呢(我现在翻墙有问题),我测试,copier 是比较慢的,这速度有点像空跑。
这是我的 test code,结果表明 copier 连两次序列化 json 的时间都比不过,性能直接垫底。。。https://github.com/antlabs/deepcopy-benchmark
个人觉得少用反射包比较好,这会破坏静态类型的可靠性,我感觉官方也是不希望我们用的。
https://gist.github.com/eltria/c273e38b7b1a528a1fe3e4920cc22215
之前的确是我的测试代码有问题,现在看起来 coven 的方案是最快的,只需要事先 new 一个 converter
是的,反射包要少用,老师傅也容易写出 bug 。
和 jinzhu/copier 对比,deepcopy 快。压测结果可看附言 1.
要支持 dst, src 不对称指针拷贝,要有个好的算法解决循环引用的问题(结构体里面有环路),deepcopy 现在用的算法,是记录指针地址。并且因为 deepcopy 是深度拷贝,要取引用 struct 。如果要支持不对称指针,遇到下面的代码就 gg 了,当然现在是没问题的。coven 是指针浅拷贝,有时间不会解引用,所以不要操这份心.
type R struct {
R *R
}
r := R{}
r.R = &r
在Go语言中,确实经常需要处理数据的赋值和拷贝操作,尤其是当数据结构变得复杂时,手动赋值不仅繁琐而且容易出错。deepcopy.Copy
的出现,为开发者提供了一种便捷且高效的深度拷贝解决方案。
深度拷贝(Deep Copy)与浅拷贝(Shallow Copy)不同,它会递归地复制所有嵌套的对象,确保新对象与原对象在内存中是完全独立的。这在处理包含指针、切片、映射等引用类型的数据结构时尤为重要,因为浅拷贝只会复制引用,而深度拷贝则会复制引用所指向的实际数据。
deepcopy.Copy
通常依赖于反射(reflection)机制来实现,这使得它能够灵活地处理各种类型的数据结构,而无需为每种类型编写专门的拷贝代码。然而,反射也会带来一定的性能开销,因此在性能敏感的场景下需要谨慎使用。
此外,使用 deepcopy.Copy
时还需要注意一些潜在的问题,比如循环引用可能导致无限递归和栈溢出,以及对于某些包含不可拷贝字段(如文件句柄、网络连接等)的结构体,深度拷贝可能会失败或产生不可预测的结果。
总的来说,deepcopy.Copy
提供了一种强大且灵活的深度拷贝解决方案,极大地简化了复杂数据结构的拷贝操作。但在使用时,开发者需要充分了解其工作原理和潜在限制,以确保代码的正确性和性能。