在golang中可以使用a := b這種方式將b賦值給a,只有當b能進行深拷貝時a與b才不會互相影響,否則就需要進行更為複雜的深拷貝。 下麵就是Go賦值操作的一個說明: Go語言中所有賦值操作都是值傳遞,如果結構中不含指針,則直接賦值就是深度拷貝;如果結構中含有指針(包括自定義指針,以及切片,map ...
在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
另外還有一個關於錯誤處理的可比較特性的坑,因此強烈建議自定義error
用指針,否則就得確保必須可比較:
package main
import (
"errors"
"fmt"
)
func main() {
err := DoSomething(true)
ok := errors.Is(err, ErrorA)
fmt.Println(ok, err)
err = DoSomething(false)
ok = errors.Is(err, ErrorB)
fmt.Println(ok, err)
}
type CustomError struct {
Metadata map[string]string
Message string
}
func (c CustomError) Error() string {
return c.Message
}
var (
// ErrorA 包含不可比較欄位,在 errors.Is 中
ErrorA = CustomError{Message: "A", Metadata: map[string]string{"Reason": "A"}}
ErrorB = &CustomError{Message: "B", Metadata: map[string]string{"Reason": "B"}}
)
func DoSomething(isA bool) error {
if isA {
return ErrorA
}
return ErrorB
}
引用
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