首页IT科技go判断类型(判断go对象是否能直接赋值进行深拷贝)

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

时间2025-05-04 07:52:33分类IT科技浏览3223
导读:在golang中可以使用a := b这种方式将b赋值给a,只有当b能进行深拷贝时a与b才不会互相影响,否则就需要进行更为复杂的深拷贝。...

在golang中可以使用a := b这种方式将b赋值给a            ,只有当b能进行深拷贝时a与b才不会互相影响                   ,否则就需要进行更为复杂的深拷贝            。

下面就是Go赋值操作的一个说明:

Go语言中所有赋值操作都是值传递      ,如果结构中不含指针            ,则直接赋值就是深度拷贝;如果结构中含有指针(包括自定义指针                   ,以及切片      ,map等使用了指针的内置类型)      ,则数据源和拷贝之间对应指针会共同指向同一块内存                   ,这时深度拷贝需要特别处理                   。目前            ,有三种方法      ,一是用gob序列化成字节序列再反序列化生成克隆对象;二是先转换成json字节序列                   ,再解析字节序列生成克隆对象;三是针对具体情况            ,定制化拷贝      。前两种方法虽然比较通用但是因为使用了reflex反射,性能比定制化拷贝要低出2个数量级                   ,所以在性能要求较高的情况下应该尽量避免使用前两者            。

现在我需要判断某个对象是否可以直接用赋值进行深拷贝                   ,如果不能直接进行深拷贝时,到底是哪个字段影响了深拷贝            ,下面就是判断的代码:

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 }

运行结果:

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的赋值操作只存在值拷贝      ,由于一些引用类型赋值的是地址导致两个变量共用内存数据才导致需要额外进行深拷贝处理      。

同理可得函数传参也是赋值            ,因此值传递时对象不能自动深拷贝也需要特殊处理                   ,看如下示例:

package main import ( "fmt" ) func main() { err := test() if err != nil { panic(err) } } type TT struct { a int b *string } func test() error { as := "123" t := TT{a: 123, b: &as} fmt.Printf("t1 %#v,%p,%s\n", t, &t, *t.b) a(t) fmt.Printf("t2 %#v,%p,%s\n", t, &t, *t.b) return nil } func a(t TT) { fmt.Printf("a1 %#v,%p,%s\n", t, &t, *t.b) *t.b = "456" fmt.Printf("a2 %#v,%p,%s\n", t, &t, *t.b) }

结果如下      ,很多人都以为函数参数为值传递时被调函数参数无法影响上层函数      ,看来这是错的:

t1 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a270,123 a1 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a2a0,123 a2 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a2a0,456 t2 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a270,456

如下所示值类型对象方法也是能够影响引用类型数据的:

package main import ( "fmt" ) func main() { bs := "123" t := TT{a: 1, b: &bs} fmt.Printf("1 %#v,%p,%s\n", t, &t, *t.b) t.A() fmt.Printf("2 %#v,%p,%s\n", t, &t, *t.b) t.B() fmt.Printf("3 %#v,%p,%s\n", t, &t, *t.b) } type TT struct { a int b *string } func (t TT) A() { *t.b = "A" } func (t TT) B() { *t.b = "B" }

结果如下:

# 虽然 A() 和 B() 都是值对象函数,但是结构体中指针类型属于引用类型 1 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,123 2 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,A 3 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,B

关于字符串的参数赋值:

package main import ( "fmt" "reflect" "unsafe" ) func main() { s := "123" sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Printf("m1 %#v,%p,%v\n", s, &s, sh.Data) a(s) b := []byte("456") s = *(*string)(unsafe.Pointer(&b)) sh = (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Printf("m2 %#v,%p,%v\n", s, &s, sh.Data) a(s) b[0] = 6 // 修改内存中的数据 sh = (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Printf("m3 %#v,%p,%v\n", s, &s, sh.Data) a(s) } func a(s string) { sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Printf("a %#v,%p,%v\n", s, &s, sh.Data) }

结论是                   ,字符串传参实际底层数据是共用的            ,因为字符串不可变逻辑      ,因此这样更省内存:

m1 "123",0xc00005a260,18648789 a "123",0xc00005a280,18648789 m2 "456",0xc00005a260,824633827584 a "456",0xc00005a2b0,824633827584 m3 "656",0xc00005a260,824633827584 a "656",0xc00005a2e0,824633827584

引用

https://www.ssgeek.com/post/golang-jie-gou-ti-lei-xing-de-shen-qian-kao-bei/

https://sorcererxw.com/articles/go-comparable-type

https://blog.csdn.net/pengpengzhou/article/details/105839518

https://www.cnblogs.com/gtea/p/16850496.html

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
3070和3070ti差多少钱(3070和3070ti有什么区别介绍) 关键词拓词的方法有哪些(如何用关键词拓词,轻松实现SEO优化?)