当前位置: 首页 > 科技观察

Go语言Append缺陷引发的深度文案讨论

时间:2023-03-17 16:37:27 科技观察

看完苏炳添进入决赛,激动得连洗手间都不敢耽误超过5分钟。在这个历史性的时刻,决定休整的我,在垂死的病痛中惊醒,开始写肝。简介什么是浅层?什么是深?深拷贝的四种方式手写拷贝函数JSON序列化反序列化gob序列化反序列化基准测试(性能测试)总结介绍今天这篇文章是从我周六加班的一个bug中引入的。上下文是在一个struct中有一个Labelsslice,在组装数据的时候需要给它加上配置变量中的label。让我们看看发生了什么。fori:=rangem{m[i].Labels=append(r.Config.Relabel,m[i].Labels...)...}debug发现i=0时正常,但是第二次和即使是第n次m[?].Labels的内容这次也会连续变化。看了append的源码发现,当容量足够的时候,append会直接把数据添加到第一个参数的slice中。改成下面的代码,换个位置,一切正常。m[i].Labels=append(m[i].Labels,r.Config.Relabel...)这是一个隐式陷阱。Go语言中的赋值副本往往是浅拷贝,开发者很容易忽略这一点,导致出现此类意想不到的问题,以后多加注意。今天的文章就从这个问题和上一篇作业中提到的深拷贝问题开始。什么叫肤浅?什么是深?很多年前在C++工作过,它的对象拷贝是浅拷贝。原理是调用了默认的拷贝构造函数,需要人为改写。复制的过程,尤其是指针的生成需要小心谨慎,避免内存泄露。后来接触了Python,发现深浅拷贝的问题在所有后端语言中都存在,Go也不例外。浅拷贝对于值类型是完整的拷贝,对于引用类型是拷贝其地址。即复制对象修改引用类型的变量也会影响源对象。这就是为什么channel在传参的时候会把内容写在里面,receiver才能接收成功。在Go中,指针、切片、通道、接口、映射和函数都是浅拷贝。最容易出问题的三种类型是指针、切片和映射。方便的一点是作为参数传递不需要取地址,可以直接修改其内容,只要函数内部没有覆盖,就不需要返回值。但是作为结构体中的成员变量,复制结构体后问题就暴露出来了。修改一个会导致另一个也发生变化。深拷贝的四种方式有一次和女朋友谈深拷贝,她告诉我,最方便的深拷贝方式就是序列化为json,然后反序列化。当我听到这个解决方案时,我感到很惊讶。确实很容易,但是由于序列化会用到反射,所以效率不会太高。深拷贝有四种方式1.手写拷贝功能2.JSON序列化和反序列化3.Gob序列化和反序列化4.使用反射github上的开源库,大部分是基于1和4两种方式优化的。这里的反射方法将在后面讨论。我的githubhttps://github.com/minibear2333/后面会写一个组件,提供各种现成的深拷贝方式。手写复制函数定义了一个包含切片、字典和指针的结构。typeFoostruct{List[]intFooMapmap[string]stringintPtr*int}手动复制函数并将其命名为Duplicatefunc(f*Foo)Duplicate()Foo{vartmp=Foo{List:make([]int,0,len(f.List)),FooMap:make(map[string]string),intPtr:new(int),}copy(tmp.List,f.List)fori:=rangef.FooMap{tmp.FooMap[i]=f.FooMap[i]}iff.intPtr!=nil{*tmp.intPtr=*f.intPtr}else{tmp.intPtr=nil}returntmp}函数copy的内部初始化结构是标准库自带的copy函数map只能range来复制,这里map为nil,不会报错。指针在使用前必须判断为空,并且必须为指针赋值,指针地址不能被覆盖。测试funcmain(){vara=1vart1=Foo{intPtr:&a}t2:=t1.Duplicate()a=2fmt.Println(*t1.intPtr)fmt.Println(*t2.intPtr)}输出显示深度复制成功21json序列化和反序列化用这种方式完成深拷贝很简单,但是结构必须要注解,并且没有私有字段typeFoostruct{List[]int`json:"list"`FooMapmap[string]string`json:"foo_map"`IntPtr*int`json:"int_ptr"`}提供了一个直接的解决方案funcDeepCopyByJson(dst,srcinterface{})error{b,err:=json.Marshal(src)iferr!=nil{returnerr}err=json.Unmarshal(b,dst)returnerr}其中src和dst是同一个结构类型,使用dst时必须取地址,因为需要改变地址指向的数据来更新值的用法,我省略了错误处理a=3t1=Foo{IntPtr:&a}t2=Foo{}_=DeepCopyByJson(&t2,t1)fmt.Println(*t1.IntPtr)fmt.Println(*t2.IntPtr)输出33gob序列化和反序列化这是标准库提供的一种编码方式,类似于protobuf,Gob(即GoBinary的缩写))类似于Python的pickle和Java的Serialization。在发送方编码,在接收方解码。funcDeepCopyByGob(dst,srcinterface{})error{varbufferbytes.Bufferiferr:=gob.NewEncoder(&buffer).Encode(src);err!=nil{returnerr}returnob.NewDecoder(&buffer).Decode(dst)}用法funcDeepCopyByGob(dst,srcinterface{})error{varbufferbytes.Bufferiferr:=gob.NewEncoder(&buffer).Encode(src);err!=nil{returnerr}returngob.NewDecoder(&buffer).Decode(dst)}输出44基准测试(性能测试)这三个方法我都写了benchmark测试用例,Go会自动反复调用,直到计算出合理的时间范围。Benchmark测试代码,这里只写一个,其他两个函数的测试方法类似:funcBenchmarkDeepCopyByJson(b*testing.B){b.StopTimer()vara=1vart1=Foo{IntPtr:&a}t2:=Foo{}b.StartTimer()fori:=0;ijson>gob复制方式相差2个数量级复制私有成员变量。如果是经常拷贝的节目,建议使用手动拷贝的方式拷贝,拷贝过??程可以自定义。甚至可以完成不同结构之间字段细微差异的定制需求。PS:内置copy和reflect.copy都只支持切片或者数组的拷贝,内置copy的速度是反射的两倍以上。扩展资料Go语言使用Gob传输数据http://c.biancheng.net/view/4597.html)内置copy函数和reflect.Copy函数的区别https://studygolang.com/topics/13523/comment/43357Benchmark测试https://segmentfault.com/a/1190000016354758本文转载自微信公众号“机智的程序员熊”,可通过以下二维码关注。转载本文请联系机智的程序员小熊公众号。