在golang
中可以使用a := b
这种方式将b
赋值给a
,只有当b
能进行深拷贝时a
与b
才不会互相影响,否则就需要进行更为复杂的深拷贝。
下面就是Go赋值操作的一个说明:
Go语言中所有赋值操作都是值传递,如果结构中不含指针,则直接赋值就是深度拷贝;如果结构中含有指针(包括自定义指针,以及切片,map等使用了指针的内置类型),则数据源和拷贝之间对应指针会共同指向同一块内存,这时深度拷贝需要特别处理。目前,有三种方法,一是用gob序列化成字节序列再反序列化生成克隆对象;二是先转换成json字节序列,再解析字节序列生成克隆对象;三是针对具体情况,定制化拷贝。前两种方法虽然比较通用但是因为使用了reflex反射,性能比定制化拷贝要低出2个数量级,所以在性能要求较高的情况下应该尽量避免使用前两者。
现在我需要判断某个对象是否可以直接用赋值进行深拷贝,如果不能直接进行深拷贝时,到底是哪个字段影响了深拷贝,下面就是判断的代码:
package main
import (
"bytes"
"fmt"
"reflect"
"unsafe"
)
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 float64
CatA
}
ccc struct {
A int
B Cat
}
)
func main() {
var out bytes.Buffer
ok := CanDeepCopy(Per{}, &out)
fmt.Println(ok, out.String(), PointerLess(Per{}))
out.Reset()
ok = CanDeepCopy(Bar{}, &out)
fmt.Println(ok, out.String(), PointerLess(Bar{}))
out.Reset()
ok = CanDeepCopy(Cat{}, &out)
fmt.Println(ok, out.String(), PointerLess(Cat{}))
out.Reset()
ok = CanDeepCopy(ccc{A: 1}, &out)
fmt.Println(ok, out.String(), PointerLess(ccc{A: 1}))
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 {
tv := reflect.TypeOf(v)
if path.Len() == 0 {
path.WriteString(tv.Name()) // 记录首次对象名称
}
switch t := tv.Kind(); t {
case reflect.Struct: // 结构体需要判断每一个字段
path.WriteByte('.')
for i, pn := 0, path.Len(); i < tv.NumField(); i++ {
tf := tv.Field(i)
path.WriteString(tf.Name) // 记录子字段名称
// 构造一个该字段类型的对象,注意将指针换成值
fv := reflect.New(tf.Type).Elem().Interface()
if !CanDeepCopy(fv, path) {
return false // 递归判断每个字段,包括匿名字段
}
path.Truncate(pn) // 回溯时截断没问题的子字段
}
return true
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.Array,
reflect.String /* string类型是特例,也是引用类型,但不可修改导致复制也没关系 */:
return true // 上述类型可以进行深拷贝
default:
path.WriteString(": ")
path.WriteString(tv.String())
path.WriteString(" is ")
path.WriteString(t.String())
path.WriteString(" no deep copy")
return false // 其他类型不能深拷贝
}
}
func PointerLess(x any) bool {
// 来自大佬的解惑: https://studygolang.com/topics/16111
// 经过测试Cat对象为false,根据解释是因为有string类型,但是字符串类型不可修改,所以是特例
// 因此最好还是按照上面的方案递归判断
var ptr uintptr
ptr = *(*uintptr)(unsafe.Pointer(uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&x))) + unsafe.Size