看完苏炳添进入总决赛,看得我热血沸腾的,上厕所都不敢耽搁超过 5 分钟。

这历史性的一刻,让本决定休息的我,垂死病中惊坐起,开始肝文章。

引子

今天的文章从我周六加班改的一个bug引入,上下文是在某个struct中有个Labels切片,在组装数据的时候需要为其加上配置变量中的标签。

大家看看会出现什么问题。

for i := range m{m[i].Labels = append(r.Config.Relabel, m[i].Labels...)...
}

debug发现,i=0时正常,但第二次乃至第n次会不断变更之前m[?].Labels的内容。

看了append的源码,原来当容量足够的时候,append会把数据直接添加到第一个参数的切片里。

改为如下代码,调换下了位置,一切正常了。

m[i].Labels = append(m[i].Labels,r.Config.Relabel...)

这是一个隐含的陷阱,在 go 语言中赋值拷贝往往都是浅拷贝,开发者很容易不小心忽视这一点,导致这种无法预料的问题出现,以后要多多注意了。

借由这个问题以及上一篇文章的作业中,提到的深度拷贝问题展开今天的文章。

何谓浅?何谓深?

我多年以前是做c++的,它的对象拷贝是浅拷贝,原理是调用了默认的拷贝构造函数,需要人为的重写,进行拷贝的过程,特别是指针需要谨慎的生成的释放,来避免内存泄露的发生。

后来接触了Python 发现深浅拷贝的问题在后端语言中都是存在的,Go 也不例外。

浅拷贝对于值类型是完全拷贝一份,而对于引用类型是拷贝其地址。也就是拷贝的对象修改引用类型的变量同样会影响到源对象。

这就是为什么channel在做参数传递的时候,向内部写入内容,接收端可以成功收到的原因。

Go中,指针slicechannelinterfacemap函数都是浅拷贝。最容易出问题的就是指针、切片、map这三种类型。

方便的点是作为参数传递不需要取地址可以直接修改其内容,只要函数内部不出现覆盖就不需要返回值。

但作为结构体中的成员变量,在拷贝结构体后问题就暴露出来了。修改一处导致另一处也变了。

深拷贝的四种方式

有一次和女朋友聊到深拷贝的问题,她告诉我最方便的深拷贝方法就是序列化为json再反序列化。

我听到这种方案,顿时惊为天人,确实挺省事的,但由于序列化会用到反射,效率自然不会太高。

深拷贝有四种方式

  • 1、手写拷贝函数
  • 2、json序列化反序列化
  • 3、gob序列化反序列化
  • 4、使用反射

github上的开源库,大多基于 1、4 两种方式做的优化。这里的反射方法后面再做讨论。

我的github https://github.com/minibear2333/ 后续会专门写一个组件,提供深度拷贝的各种现成的方式。

手写拷贝函数

定义一个包含切片、字典、指针的结构体。

type Foo struct {List   []intFooMap map[string]stringintPtr *int
}

手动拷贝函数,把它取名为Duplicate

func (f *Foo) Duplicate() Foo {var tmp = Foo{List:   make([]int, 0, len(f.List)),FooMap: make(map[string]string),intPtr: new(int),}copy(tmp.List, f.List)for i := range f.FooMap {tmp.FooMap[i] = f.FooMap[i]}if f.intPtr != nil {*tmp.intPtr = *f.intPtr} else {tmp.intPtr = nil}return tmp
}
  • 函数内部初始化结构体
  • copy是标准库自带的拷贝函数
  • map只能range来拷贝,这里mapnil不会报错
  • 指针使用前必须判空,为指针的指向赋值,而不能覆盖指针地址

测试

func main() {var a = 1var t1 = Foo{intPtr: &a}t2 := t1.Duplicate()a = 2fmt.Println(*t1.intPtr)fmt.Println(*t2.intPtr)
}

输出说明深拷贝成功

2
1

json序列化反序列化

这种方式完成深度拷贝非常简单,但必须结构体加上注解,而且不允许出现私有字段

type Foo struct {List   []int             `json:"list"`FooMap map[string]string `json:"foo_map"`IntPtr *int              `json:"int_ptr"`
}

提供一个直接的方案

func DeepCopyByJson(dst, src interface{}) error {b, err := json.Marshal(src)if err != nil {return err}err = json.Unmarshal(b, dst)return err
}
  • 其中srcdst是同一种结构体类型
  • dst使用时必须取地址,因为要给地址指向的数据变更新值

用法,我省略了错误处理

a = 3
t1 = Foo{IntPtr: &a}
t2 = Foo{}
_ = DeepCopyByJson(&t2, t1)
fmt.Println(*t1.IntPtr)
fmt.Println(*t2.IntPtr)

输出

3
3

gob序列化反序列化

这是一种标准库提供的编码方法,类似于protobuf,Gob(即 Go binary 的缩写)。类似于 PythonpickleJavaSerialization

在发送端编码,接收端解码。

func DeepCopyByGob(dst, src interface{}) error {var buffer bytes.Bufferif err := gob.NewEncoder(&buffer).Encode(src); err != nil {return err}return gob.NewDecoder(&buffer).Decode(dst)
}

用法

a = 4
t1 = Foo{IntPtr: &a}
t2 = Foo{}
_ = DeepCopyByGob(&t2, t1)
fmt.Println(*t1.IntPtr)
fmt.Println(*t2.IntPtr)

输出

4
4

基准测试(性能测试)

这三种方式我分别写了基准测试的测试用例,go会自动反复调用,直到测算出一个合理的时间范围。

基准测试代码,这里仅写一个,其他两个函数的测试方式类似:

func BenchmarkDeepCopyByJson(b *testing.B) {b.StopTimer()var a = 1var t1 = Foo{IntPtr: &a}t2 := Foo{}b.StartTimer()for i := 0; i < b.N; i++ {_ = DeepCopyByJson(&t2, t1)}
}

运行测试

$ go test -test.bench=. -cpu=1,16  -benchtime=2s
goos: darwin
goarch: amd64
pkg: my_copy
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkFoo_Duplicate          35887767                62.64 ns/op
BenchmarkFoo_Duplicate-16       37554250                62.56 ns/op
BenchmarkDeepCopyByGob            104292             22941 ns/op
BenchmarkDeepCopyByGob-16         103060             23049 ns/op
BenchmarkDeepCopyByJson          2052482              1171 ns/op
BenchmarkDeepCopyByJson-16       2057090              1175 ns/op
PASS
ok      my_copy 17.166s
  • mac环境下单核和多核并没有明显差异
  • 运行速度快慢,手动拷贝方式 > json > gob
  • 拷贝方式都相差了 2 个数量级

小结

如果是偶尔使用的程序可以使用json序列化反序列化的方式进行拷贝,但是除了慢以外还有一个缺陷,就是无法拷贝私有成员变量。

如果是频繁拷贝的程序,建议使用手动拷贝方式进行拷贝,而且可以定制化拷贝的过程。甚至可以完成不同结构体之间,字段细微差异的定制化需求。

PS:内置copyreflect.copy都只支持切片或数组的拷贝,内置copy速度是反射方式的两倍以上。

拓展资料

  • Go 语言使用 Gob 传输数据 http://c.biancheng.net/view/4597.html)
  • 内建copy函数和reflect.Copy函数的区别 https://studygolang.com/topics/13523/comment/43357
  • 基准测试 https://segmentfault.com/a/1190000016354758

往期精彩回顾

  • Go标准库:json解析陷阱与版本变动时的偷懒技巧
  • 讲透Go中的并发接收控制结构select
  • Go异常处理详解
  • Go结构体的思考
  • Go并发等待

没想到文章写完还是到了第二天,原创不易,点个赞支持一下,爱你么么!

这次简单提到了基准测试,下一次我们展开详细学习单元测试与基准测试的内容,我们下次见!

希望您可以帮我点个赞,双击一下,鼓励一下我,这对我很重要,谢谢姐妹!

Go语言append缺陷引发的深度拷贝讨论相关推荐

  1. Python直接赋值,浅拷贝和深度拷贝

    查阅得: 直接赋值:其实就是对象的引用(别名). 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象. 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象 ...

  2. Python 直接赋值、浅拷贝和深度拷贝区别

    b = a: 赋值引用,a 和 b 都指向同一个对象. b = a.copy(): 浅拷贝, a 和 b 是一个独立的对象,但他们的子对象还是指向同一对象(是引用) ''' 遇到问题没人解答?小编创建 ...

  3. 拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

     1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: ...

  4. Python 直接赋值、浅拷贝和深度拷贝解析

    转自菜鸟教程: https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-deep.html 直接赋值:其实就是 ...

  5. java序列化与深度拷贝

    [README] 1, 为啥要序列化或序列化的意义? 2,系统间调用的报文格式,大多数是Json字符串(或字节数组):接收方接收json: 3,但当系统调用如RMI,客户端请求服务器获取一个 java ...

  6. python中浅拷贝和深度拷贝的区别

    在很多面试题中都会问到浅拷贝跟深度拷贝的区别,前几天一个朋友也问到了我浅拷贝跟深度拷贝到底有什么区别,这里就简单举栗子讲一下两者的区别. 浅拷贝(copy()):拷贝父对象,不会拷贝对象的内部的子对象 ...

  7. Python 直接赋值、浅拷贝和深度拷贝全解析

    直接赋值:其实就是对象的引用(别名,其实就是一个人今天叫张三 明天叫张狗子的意思). 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象. 深拷贝(deepcopy): copy 模块的 de ...

  8. python深度复制_Python直接赋值、浅拷贝和深度拷贝解析

    直接赋值:其实就是对象的引用(别名). 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象. 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象 ...

  9. 2018首届传神者大会:“语言+新技术”将推动语言产业生态化发展 2018首届传神者大会圆满落幕,“语言+新技术”或开启全球深度互联时代 智联未来,跨界赋能 1211首届传神者大会圆满落幕...

    2018首届传神者大会:"语言+新技术"将推动语言产业生态化发展 2018首届传神者大会圆满落幕,"语言+新技术"或开启全球深度互联时代 智联未来,跨界赋能 1 ...

最新文章

  1. host文件修改后无法保存的问题
  2. 1.3 用神经网络进行监督学习-深度学习-Stanford吴恩达教授
  3. Python,得到列表最小k个数或最大k个数的索引
  4. 完全卸载mysql数据库图文教程
  5. Java中的wait()和sleep()方法之间的区别
  6. 329. 矩阵中的最长递增路径
  7. 使用 jQuery Mobile 与 HTML5 开发 Web App (二) —— jQuery Mobile 基础
  8. 解决Eclipse建Maven项目module无法转换为2.5
  9. 上银驱动器使用手册_禾川伺服驱动器说明书
  10. 语法冠词,虚拟语气,形容词排序
  11. HTML个人网站设计(源码)
  12. #华为云#听从你心,无问西东
  13. 优惠券管理--优惠券类型
  14. RK3399平台开发系列讲解(USB网卡)5.48、USBNET的CDC link on/off 消息
  15. 利用python对gif图片进行压缩处理,简单案例
  16. python爬虫小工具——下载助手
  17. VisionPro斑点工具CogBlobTool
  18. UndoManager教程
  19. 最强汉字得到首字母拼音java版
  20. 线性系统大作业——2.二阶倒立摆建模与控制系统设计(上)

热门文章

  1. python编程图形_Python编程与几何图形
  2. MFC扩展库BCGControlBar Pro v33.5新版亮点 - 控件、脚本管理增强
  3. ImageView scaleType 各种不同效果解析
  4. 陈欧再为自己代言 聚美优品超跌反弹涨7.96%
  5. EPLAN2022——连接定义点
  6. 再看《肖申克的救赎》
  7. Unity3D网络游戏实战——通用客户端模块
  8. Cisco Wireless中显示的slot0, slot1是什么含义
  9. 【转】全球第一经典语录,每天读一遍
  10. 评析美媒预测2018年手机将有的15个未来功能