V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
guonaihong
V2EX  ›  Go 编程语言

向繁琐的赋值代码说不。deepcopy.Copy 深度拷贝来了

  •  
  •   guonaihong ·
    guonaihong · 2020-05-07 09:32:01 +08:00 · 6177 次点击
    这是一个创建于 1655 天前的主题,其中的信息可能已经有所发展或是发生改变。

    出发点

    一次帮同事 review 代码,想在 go 里面找一个支持深度 Copy 的库,github 上少得可怜。最后找到 json marshal 加 unmarshal 的方式,但是这种方式有两个缺点,第 1 marshal 一次 reflect,unmarshal 一次 reflect,有两次 reflect 的过程,效率会垫底。第 2,不支持过滤条件,这点硬伤,改不了。特对这两点问题,所以想撸个改进版本(更快,更可控)。

    项目地址

    https://github.com/antlabs/deepcopy

    作用

    Go codecov

    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
    
    
    第 1 条附言  ·  2020-05-17 16:23:37 +08:00
    压测结果。
    https://github.com/antlabs/deepcopy-benchmark

    deepcopy 下个版本会使用 ptr 方式重新优化一个版本。
    28 条回复    2020-05-17 16:35:22 +08:00
    yuyoung
        1
    yuyoung  
       2020-05-07 09:49:27 +08:00
    咋看着提升不是很明显
    tcfenix
        2
    tcfenix  
       2020-05-07 10:00:36 +08:00
    试着对比了一下 jsoniter
    要不要试着做一下缓存?
    tcfenix
        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

    图片贴不出来,这样看一下吧
    guonaihong
        4
    guonaihong  
    OP
       2020-05-07 10:05:26 +08:00
    @tcfenix jsoniter 里面也用的 reflect API ?晚上我加下缓存优化下。
    tcfenix
        5
    tcfenix  
       2020-05-07 10:11:48 +08:00
    @guonaihong
    jsoniter 第一次会反射,但是反射出来的结果会缓存

    其实这样代码生成的方式也挺不错的,牺牲掉一点维护性也是可以接受的
    https://github.com/globusdigital/deep-copy

    当然,golang 没有像 BeanCopier 这样的神器的确是比较可惜了...
    guonaihong
        6
    guonaihong  
    OP
       2020-05-07 10:15:12 +08:00
    @guonaihong 可否把你的 benckmark 代码发下。我优化下,再看下性能。
    guonaihong
        7
    guonaihong  
    OP
       2020-05-07 10:19:38 +08:00
    @yuyoung 标准库里面的代码做了缓存,所有第一个版本只领先了 18%-30%。如果用同样的思路优化,领先的会更多。
    毕竟序列化,反序列化的方式深度拷贝要两次 reflect 。
    guonaihong
        9
    guonaihong  
    OP
       2020-05-07 10:48:08 +08:00
    @tcfenix 谢了。
    pmispig
        10
    pmispig  
       2020-05-07 11:01:04 +08:00
    go 原生赋值就是深拷贝啊,你这个是标题党吧。
    你这个最多算是异构赋值
    guonaihong
        11
    guonaihong  
    OP
       2020-05-07 11:18:52 +08:00
    @pmispig slice, map 可以深度拷贝?
    guonaihong
        12
    guonaihong  
    OP
       2020-05-07 11:19:47 +08:00
    @pmispig 结构体里面套指针,套 interface{},套 slice,套 map,不可以深度拷贝。
    useben
        13
    useben  
       2020-05-07 11:40:06 +08:00
    和 jinzhu/copier 对比下?
    guonaihong
        14
    guonaihong  
    OP
       2020-05-07 11:48:26 +08:00
    @useben 好,会压测下,结果到时候通知。
    Kisesy
        15
    Kisesy  
       2020-05-07 12:29:27 +08:00
    不支持多重指针, 比如一个 *int 字段往 **int 字段赋值, 就会报错, 如果用 json 包可以处理
    这种情况 jinzhu/copier 也不支持, 但 github.com/petersunbag/coven 支持, 而且更快? 希望楼主加入支持后, 再压测一下
    rrfeng
        16
    rrfeng  
       2020-05-07 12:37:47 +08:00
    我只有一个疑问:
    支持 tag 是不是多余了?我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗??
    我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……
    guonaihong
        17
    guonaihong  
    OP
       2020-05-07 13:37:33 +08:00
    @rrfeng hi rrfeng 。不加 tag 可以直接拷贝的。所有 ->“我要是能在源结构里加 tag,直接写个 copy 方法不爽快吗??”,所以,不 tag,不需要写 copy 方法会更更爽快。。。

    从 ->"我觉得一个完整的工程里很难用到 deepcopy 这种方法,更多的是用别人的数据结构,然后想复制一份出来操作避免侵入原数据,所以 tag 毫无用武之地……" ,这里说了 if 的情况,所以 else 也是有点用的,比如都是自己的包,刚好要过滤几个字段。。。
    blackboom
        18
    blackboom  
       2020-05-07 13:47:00 +08:00
    链式调用重构一下?不然都是 Do
    ```
    deepcopy.RegisterTagName("copy").Copy(&d, &s)
    ```
    guonaihong
        19
    guonaihong  
    OP
       2020-05-07 13:48:08 +08:00
    @blackboom ok, 我思考下。
    tcfenix
        20
    tcfenix  
       2020-05-07 14:12:27 +08:00
    tcfenix
        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

    试了一下刚才看到的两个库,效果非常好
    guonaihong
        22
    guonaihong  
    OP
       2020-05-07 14:18:55 +08:00
    @tcfenix 测试错了吧,把代码贴到 V2EX 呢(我现在翻墙有问题),我测试,copier 是比较慢的,这速度有点像空跑。
    guonaihong
        23
    guonaihong  
    OP
       2020-05-07 15:05:46 +08:00
    @tcfenix 这是我的 test code,结果表明 copier 连两次序列化 json 的时间都比不过,性能直接垫底。。。https://github.com/antlabs/deepcopy-benchmark
    lewinlan
        24
    lewinlan  
       2020-05-08 01:32:11 +08:00 via Android
    个人觉得少用反射包比较好,这会破坏静态类型的可靠性,我感觉官方也是不希望我们用的。
    tcfenix
        25
    tcfenix  
       2020-05-08 14:42:28 +08:00
    https://gist.github.com/eltria/c273e38b7b1a528a1fe3e4920cc22215

    之前的确是我的测试代码有问题,现在看起来 coven 的方案是最快的,只需要事先 new 一个 converter
    guonaihong
        26
    guonaihong  
    OP
       2020-05-09 13:59:14 +08:00
    @lewinlan 是的,反射包要少用,老师傅也容易写出 bug 。
    guonaihong
        27
    guonaihong  
    OP
       2020-05-17 16:25:07 +08:00
    @useben 和 jinzhu/copier 对比,deepcopy 快。压测结果可看附言 1.
    guonaihong
        28
    guonaihong  
    OP
       2020-05-17 16:35:22 +08:00
    @Kisesy 要支持 dst, src 不对称指针拷贝,要有个好的算法解决循环引用的问题(结构体里面有环路),deepcopy 现在用的算法,是记录指针地址。并且因为 deepcopy 是深度拷贝,要取引用 struct 。如果要支持不对称指针,遇到下面的代码就 gg 了,当然现在是没问题的。coven 是指针浅拷贝,有时间不会解引用,所以不要操这份心.
    type R struct {
    R *R
    }

    r := R{}
    r.R = &r
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1822 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 16:18 · PVG 00:18 · LAX 08:18 · JFK 11:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.