看完苏炳添进入总决赛,看得我热血沸腾的,上厕所都不敢耽搁超过 5 分钟。
创新互联公司专注于企业成都营销网站建设、网站重做改版、娄底网站定制设计、自适应品牌网站建设、成都h5网站建设、商城网站制作、集团公司官网建设、外贸营销网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为娄底等各大城市提供网站开发制作服务。
这历史性的一刻,让本决定休息的我,垂死病中惊坐起,开始肝文章。
今天的文章从我周六加班改的一个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中,指针、slice、channel、interface、map、函数都是浅拷贝。最容易出问题的就是指针、切片、map这三种类型。
方便的点是作为参数传递不需要取地址可以直接修改其内容,只要函数内部不出现覆盖就不需要返回值。
但作为结构体中的成员变量,在拷贝结构体后问题就暴露出来了。修改一处导致另一处也变了。
有一次和女朋友聊到深拷贝的问题,她告诉我最方便的深拷贝方法就是序列化为json再反序列化。
我听到这种方案,顿时惊为天人,确实挺省事的,但由于序列化会用到反射,效率自然不会太高。
深拷贝有四种方式
github上的开源库,大多基于 1、4 两种方式做的优化。这里的反射方法后面再做讨论。
我的github https://github.com/minibear2333/ 后续会专门写一个组件,提供深度拷贝的各种现成的方式。
定义一个包含切片、字典、指针的结构体。
- type Foo struct {
- List []int
- FooMap map[string]string
- intPtr *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
- }
测试
- func main() {
- var a = 1
- var t1 = Foo{intPtr: &a}
- t2 := t1.Duplicate()
- a = 2
- fmt.Println(*t1.intPtr)
- fmt.Println(*t2.intPtr)
- }
输出说明深拷贝成功
- 2
- 1
这种方式完成深度拷贝非常简单,但必须结构体加上注解,而且不允许出现私有字段
- 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
- }
用法,我省略了错误处理
- a = 3
- t1 = Foo{IntPtr: &a}
- t2 = Foo{}
- _ = DeepCopyByJson(&t2, t1)
- fmt.Println(*t1.IntPtr)
- fmt.Println(*t2.IntPtr)
输出
- 3
- 3
这是一种标准库提供的编码方法,类似于protobuf,Gob(即 Go binary 的缩写)。类似于 Python 的pickle和 Java 的Serialization。
在发送端编码,接收端解码。
- func DeepCopyByGob(dst, src interface{}) error {
- var buffer bytes.Buffer
- if err := gob.NewEncoder(&buffer).Encode(src); err != nil {
- return err
- }
- return gob.NewDecoder(&buffer).Decode(dst)
- }
用法
- func DeepCopyByGob(dst, src interface{}) error {
- var buffer bytes.Buffer
- if err := gob.NewEncoder(&buffer).Encode(src); err != nil {
- return err
- }
- return gob.NewDecoder(&buffer).Decode(dst)
- }
输出
- 4
- 4
这三种方式我分别写了基准测试的测试用例,go会自动反复调用,直到测算出一个合理的时间范围。
基准测试代码,这里仅写一个,其他两个函数的测试方式类似:
- func BenchmarkDeepCopyByJson(b *testing.B) {
- b.StopTimer()
- var a = 1
- var 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
如果是偶尔使用的程序可以使用json序列化反序列化的方式进行拷贝,但是除了慢以外还有一个缺陷,就是无法拷贝私有成员变量。
如果是频繁拷贝的程序,建议使用手动拷贝方式进行拷贝,而且可以定制化拷贝的过程。甚至可以完成不同结构体之间,字段细微差异的定制化需求。
PS:内置copy和reflect.copy都只支持切片或数组的拷贝,内置copy速度是反射方式的两倍以上。
拓展资料
本文转载自微信公众号「机智的程序员小熊」,可以通过以下二维码关注。转载本文请联系机智的程序员小熊公众号。
当前标题:Go语言Append缺陷引发的深度拷贝讨论
文章源于:http://www.shufengxianlan.com/qtweb/news48/72048.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联