一次帮同事 review 代码,想在 go 里面找一个支持深度 Copy 的库,github 上少得可怜。最后找到 json marshal 加 unmarshal 的方式,但是这种方式有两个缺点,第 1 marshal 一次 reflect,unmarshal 一次 reflect,有两次 reflect 的过程,效率会垫底。第 2,不支持过滤条件,这点硬伤,改不了。特对这两点问题,所以想撸个改进版本(更快,更可控)。
https://github.com/antlabs/deepcopy
deepcopy.Copy 主要用于两个类型间的深度拷贝[从零实现]
go get github.com/antlabs/deepcopy
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)
   
}
如果 src 的结构体嵌套了两套,MaxDepth 可以控制只拷贝一层
deepcopy.Copy(&dst{}, &src{}).MaxDepth(1).Do()
只拷贝结构体里面有 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)
}
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)
}
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
     1 
                    
                    yuyoung      2020-05-07 09:49:27 +08:00 
                    
                    咋看着提升不是很明显 
                 | 
            
     2 
                    
                    tcfenix      2020-05-07 10:00:36 +08:00 
                    
                    试着对比了一下 jsoniter 
                要不要试着做一下缓存?  | 
            
     3 
                    
                    tcfenix      2020-05-07 10:02:24 +08:00 
                    
                    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 图片贴不出来,这样看一下吧  | 
            
     4 
                    
                    guonaihong   OP @tcfenix jsoniter 里面也用的 reflect API ?晚上我加下缓存优化下。 
                 | 
            
     5 
                    
                    tcfenix      2020-05-07 10:11:48 +08:00 
                    
                    @guonaihong  
                jsoniter 第一次会反射,但是反射出来的结果会缓存 其实这样代码生成的方式也挺不错的,牺牲掉一点维护性也是可以接受的 https://github.com/globusdigital/deep-copy 当然,golang 没有像 BeanCopier 这样的神器的确是比较可惜了...  | 
            
     6 
                    
                    guonaihong   OP @guonaihong 可否把你的 benckmark 代码发下。我优化下,再看下性能。 
                 | 
            
     7 
                    
                    guonaihong   OP @yuyoung 标准库里面的代码做了缓存,所有第一个版本只领先了 18%-30%。如果用同样的思路优化,领先的会更多。 
                毕竟序列化,反序列化的方式深度拷贝要两次 reflect 。  | 
            
     8 
                    
                    tcfenix      2020-05-07 10:22:07 +08:00 
                    
                    
                 | 
            
     9 
                    
                    guonaihong   OP @tcfenix 谢了。 
                 | 
            
     10 
                    
                    pmispig      2020-05-07 11:01:04 +08:00 
                    
                    go 原生赋值就是深拷贝啊,你这个是标题党吧。 
                你这个最多算是异构赋值  | 
            
     11 
                    
                    guonaihong   OP @pmispig slice, map 可以深度拷贝? 
                 | 
            
     12 
                    
                    guonaihong   OP @pmispig 结构体里面套指针,套 interface{},套 slice,套 map,不可以深度拷贝。 
                 | 
            
     13 
                    
                    useben      2020-05-07 11:40:06 +08:00 
                    
                    和 jinzhu/copier 对比下? 
                 | 
            
     14 
                    
                    guonaihong   OP @useben 好,会压测下,结果到时候通知。 
                 | 
            
     15 
                    
                    Kisesy      2020-05-07 12:29:27 +08:00 
                    
                    不支持多重指针, 比如一个 *int 字段往 **int 字段赋值, 就会报错, 如果用 json 包可以处理 
                这种情况 jinzhu/copier 也不支持, 但 github.com/petersunbag/coven 支持, 而且更快? 希望楼主加入支持后, 再压测一下  | 
            
     16 
                    
                    rrfeng      2020-05-07 12:37:47 +08:00 
                    
                    我只有一个疑问: 
                支持 tag 是不是多余了?我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗?? 我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……  | 
            
     17 
                    
                    guonaihong   OP @rrfeng hi rrfeng 。不加 tag 可以直接拷贝的。所有 ->“我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗??”,所以,不 tag,不需要写 copy 方法会更更爽快。。。 
                从 ->"我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……" ,这里说了 if 的情况,所以 else 也是有点用的,比如都是自己的包,刚好要过滤几个字段。。。  | 
            
     18 
                    
                    blackboom      2020-05-07 13:47:00 +08:00 
                    
                    链式调用重构一下?不然都是 Do 
                ``` deepcopy.RegisterTagName("copy").Copy(&d, &s) ```  | 
            
     19 
                    
                    guonaihong   OP @blackboom ok, 我思考下。 
                 | 
            
     20 
                    
                    tcfenix      2020-05-07 14:12:27 +08:00 
                    
                    
                 | 
            
     21 
                    
                    tcfenix      2020-05-07 14:13:05 +08:00 
                    
                    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 试了一下刚才看到的两个库,效果非常好  | 
            
     22 
                    
                    guonaihong   OP @tcfenix 测试错了吧,把代码贴到 V2EX 呢(我现在翻墙有问题),我测试,copier 是比较慢的,这速度有点像空跑。 
                 | 
            
     23 
                    
                    guonaihong   OP @tcfenix 这是我的 test code,结果表明 copier 连两次序列化 json 的时间都比不过,性能直接垫底。。。https://github.com/antlabs/deepcopy-benchmark 
                 | 
            
     24 
                    
                    lewinlan      2020-05-08 01:32:11 +08:00 via Android 
                    
                    个人觉得少用反射包比较好,这会破坏静态类型的可靠性,我感觉官方也是不希望我们用的。 
                 | 
            
     25 
                    
                    tcfenix      2020-05-08 14:42:28 +08:00 
                    
                    https://gist.github.com/eltria/c273e38b7b1a528a1fe3e4920cc22215 
                之前的确是我的测试代码有问题,现在看起来 coven 的方案是最快的,只需要事先 new 一个 converter  | 
            
     26 
                    
                    guonaihong   OP @lewinlan 是的,反射包要少用,老师傅也容易写出 bug 。 
                 | 
            
     27 
                    
                    guonaihong   OP @useben 和 jinzhu/copier 对比,deepcopy 快。压测结果可看附言 1. 
                 | 
            
     28 
                    
                    guonaihong   OP @Kisesy 要支持 dst, src 不对称指针拷贝,要有个好的算法解决循环引用的问题(结构体里面有环路),deepcopy 现在用的算法,是记录指针地址。并且因为 deepcopy 是深度拷贝,要取引用 struct 。如果要支持不对称指针,遇到下面的代码就 gg 了,当然现在是没问题的。coven 是指针浅拷贝,有时间不会解引用,所以不要操这份心. 
                type R struct { R *R } r := R{} r.R = &r  |