判断go对象是否能直接赋值进行深拷贝

jan-bar · · 2613 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

https://www.cnblogs.com/janbar/p/17072751.html 在`golang`中可以使用`a := b`这种方式将`b`赋值给`a`,只有当`b`能进行深拷贝时`a`与`b`才不会互相影响,否则就需要进行更为复杂的深拷贝。 下面就是Go赋值操作的一个说明: Go语言中所有赋值操作都是值传递,如果结构中不含指针,则直接赋值就是深度拷贝;如果结构中含有指针(包括自定义指针,以及切片,map等使用了指针的内置类型),则数据源和拷贝之间对应指针会共同指向同一块内存,这时深度拷贝需要特别处理。目前,有三种方法,一是用gob序列化成字节序列再反序列化生成克隆对象;二是先转换成json字节序列,再解析字节序列生成克隆对象;三是针对具体情况,定制化拷贝。前两种方法虽然比较通用但是因为使用了reflex反射,性能比定制化拷贝要低出2个数量级,所以在性能要求较高的情况下应该尽量避免使用前两者。 现在我需要判断某个对象是否可以直接用赋值进行深拷贝,如果不能直接进行深拷贝时,到底是哪个字段影响了深拷贝,下面就是判断的代码: ```go package main import ( "bytes" "fmt" "reflect" ) type ( PerA struct { A int B string c []byte } Per struct { PerA Name string Age int } BarA struct { A string b *int } Bar struct { A int64 BarA } CatA struct { name string age int } Cat struct { name string age int CatA } ) func main() { var out bytes.Buffer ok := CanDeepCopy(Per{}, &out) fmt.Println(ok, out.String()) out.Reset() ok = CanDeepCopy(Bar{}, &out) fmt.Println(ok, out.String()) out.Reset() ok = CanDeepCopy(Cat{}, &out) fmt.Println(ok, out.String()) bi := 1 b0 := Bar{A: 1, BarA: BarA{A: "11", b: &bi}} b1 := b0 b1.A, b1.BarA.A, *b1.BarA.b = 2, "22", 2 fmt.Printf("%#v,%p,%d\n", b0, &b0, *b0.BarA.b) fmt.Printf("%#v,%p,%d\n", b1, &b1, *b1.BarA.b) c0 := Cat{name: "1", age: 1, CatA: CatA{name: "1", age: 1}} c1 := c0 c1.name, c1.age, c1.CatA.name, c1.CatA.age = "2", 2, "2", 2 fmt.Printf("%#v,%p\n", c0, &c0) fmt.Printf("%#v,%p\n", c1, &c1) } func CanDeepCopy(v any, path *bytes.Buffer) bool { t := reflect.TypeOf(v) if path.Len() == 0 { path.WriteString(t.Name()) // 记录首次对象名称 } switch t.Kind() { case reflect.Pointer: // 指针可比较,但不能深拷贝 path.WriteString(" is pointer") // 该字段为指针 return false case reflect.Struct: // 结构体需要判断每一个字段 path.WriteByte('.') for i, pn := 0, path.Len(); i < t.NumField(); i++ { tf := t.Field(i) path.WriteString(tf.Name) // 记录子字段名称 // 构造一个该字段类型的对象,注意将指针换成值 fv := reflect.New(tf.Type).Elem().Interface() if !CanDeepCopy(fv, path) { return false // 递归判断每个字段,包括匿名字段 } path.Truncate(pn) // 回溯时截断没问题的子字段 } } if t.Comparable() { return true } path.WriteString(" incomparable") // 该字段不可比较 return false } ``` 运行结果: ```go false Per.PerA.c incomparable # 说明 Per.a.c.cc 字段属于不可比较字段导致不能深拷贝 false Bar.BarA.b is pointer # 说明 Bar.BarA.b 字段是指针导致不能深拷贝 true Cat. # 说明 Cat 对象可以直接进行深拷贝 # 由于 Bar 不可以深拷贝 # 可以看到 b1 := b0 之后,两个对象共用 BarA.b 指针指向对象,因此 *b1.BarA.b = 2 之后也影响了b0 main.Bar{A:1, BarA:main.BarA{A:"11", b:(*int)(0xc0000a6148)}},0xc0000a03e0,2 main.Bar{A:2, BarA:main.BarA{A:"22", b:(*int)(0xc0000a6148)}},0xc0000a0400,2 # 由于 Cat 可以深拷贝,因此 c1 := c0 之后这两个对象互不影响,这种对象直接赋值,不用其他方案进行深拷贝 main.Cat{name:"1", age:1, CatA:main.CatA{name:"1", age:1}},0xc0000bc5d0 main.Cat{name:"2", age:2, CatA:main.CatA{name:"2", age:2}},0xc0000bc600 ``` 通过研究go赋值逻辑,理解了深拷贝和浅拷贝的逻辑。实际上go的赋值操作只存在值拷贝,由于一些引用类型赋值的是地址导致两个变量共用内存数据才导致需要额外进行深拷贝处理。

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2613 次点击  
加入收藏 微博
9 回复  |  直到 2023-01-31 15:40:15
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传