Go 語言傳值和深淺複製問題
關於傳值還是傳引用的問題,在go 的呼叫規範中有提到,在函式呼叫中是傳值的(是否有其他的例外情況,我們後面再來考察),但是在下面的例子中,我們將看到大部分情況下,這句話是好理解的,但是還是會有意外。那麼這句話應該怎麼理解呢?
先來看看這個例子:
1. 一個例子
type Color int
const (
black = iota
green
blue
)
type Cat struct {
name string
age int
legs Legs
eye map[string]Eye
tail Tail
}
type Eye struct {
color Color
}
type Tail struct {
length float64
}
type Legs struct {
count int
length []int
}
這隻貓融合了好幾種型別的資料,包括基本型別,內嵌自定義物件,map,slice。將 Legs 而不是 Leg 定義為一個物件是有意而為之,是為了測試一個內嵌物件中包含的基本型別和 slice 型別時的複製行為。
func ModifyCat(cat Cat) {
fmt.Printf("enter value cat:%v\n", cat)
fmt.Printf("in modify: cat:%p, cat.name:%p, cat.tail:%p\n", &cat, &cat.name, &cat.tail)
fmt.Printf("in modify: cat.legs.length:%p, cat.legs.count:%p\n", &cat.legs.length, &cat.legs.count)
cat.name = "Ben"
cat.eye["left"] = Eye{blue}
cat.tail = Tail{234.56}
cat.legs.count = 3
cat.legs.length[0] = 0
fmt.Printf("exit value cat:%v\n\n", cat)
}
func main() {
catA := Cat{
name: "tom",
age: 1,
legs: Legs{count: 4, length: []int{10, 10, 10, 10}},
eye: map[string]Eye{"left":{black}, "right":{green}},
tail: Tail{123.45},
}
fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n", &catA.legs.length, &catB.legs.length)
fmt.Println()
ModifyCat(catB)
fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n\n", &catA.legs.length, &catB.legs.length)
catB.name = "Ben"
fmt.Printf("in modify: cat: %p\n", &catB)
catB.eye["right"] = Eye{black}
catB.tail = Tail{234.56}
catB.legs.count = 3
catB.legs.length[1] = 0
fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n", &catA.legs.length, &catB.legs.length)
}
我們首先建立了一直貓 catA,然後建立一個 catB,並直接將 catA 賦值給 catB,然後將 catB 傳給 ModifyCat(Cat) 函式修改這隻新貓的模樣。並列印出前後的過程。
我們看到的具體輸出如下:
value catA:{tom 1 {4 [10 10 10 10]} map[left:{0} right:{1}] {123.45}}, catB:{tom 1 {4 [10 10 10 10]} map[left:{0} right:{1}] {123.45}}
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200
enter value cat:{tom 1 {4 [10 10 10 10]} map[right:{1} left:{0}] {123.45}}
in modify: cat:0xc8200162d0, cat.name:0xc8200162d0, cat.tail:0xc820016310
in modify: cat.legs.length:0xc8200162f0, cat.legs.count:0xc8200162e8
exit value cat:{Ben 1 {3 [0 10 10 10]} map[left:{2} right:{1}] {234.56}}
value catA:{tom 1 {4 [0 10 10 10]} map[left:{2} right:{1}] {123.45}}, catB:{tom 1 {4 [0 10 10 10]} map[left:{2} right:{1}] {123.45}}
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200
value catA:{tom 1 {4 [0 0 10 10]} map[left:{2} right:{0}] {123.45}}, catB:{Ben 1 {3 [0 0 10 10]} map[right:{0} left:{2}] {234.56}}
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200
根據輸出可以看到幾點:
- catB 進行賦值之後,不管是 cat 本身,還是 cat 內部的各個屬性的地址都已經改變了。
- 在函式內列印 cat 的幾個屬性的地址,可以看到和傳入之前的 catB 是不同的。
- 在函式內對 cat 的名字修改後,並沒有影響 catB,cat 是傳值的。
- 通過賦值得到的 catB,直接修改 catB 的名字,也沒有影響 catA 的名字。
- 這些看似符合 go 規範的說法。
幾點意外:
- 我們在函式內對 cat 的左眼的修改,影響到了外部的 catB,甚至影響到了 catA!
- 在外部對 catB 的右眼的修改,影響到了 catA
- 在函式內外對 cat 的腿部的長度 (slice 型別) 的修改,都和眼睛有一樣的效果。但是對腿部的數量 (int 型別) 的修改則沒有。
- 這些看起來可不像傳值該有的表現!!!
要理解這個問題,先來了解 slice 的底層結構。
2. 理解 Slice
slice 包括三個部分,一個指標 ptr,指向 slice 的第一個元素;一個長度 len 表示 slice 的長度,一個容量 cap 表示 slice 的容量。如下圖:
而 slice 的指標所指向 “第一個元素” 實際上是一個底層陣列的第一個元素。可能會有多個 slice 共享著同一個底層陣列。這種方式導致對 slice 的擷取,拼接等操作都異常高效,可以在常數時間內完成。
考慮下面的陣列,這樣的陣列可以通過make([]byte, 5)
來建立:
然後對該陣列執行s = s[2:4]
操作:
對於底層陣列而言一次都沒變,僅僅是 slice 的頭部三個元素變發生了變化。
容量和長度的關係是 len<=cap。一個 slice 的 cap 就是底層陣列的長度。當 len>cap(比如做了一次很長的 append) 的時候,就需要構造一個容量更長的底層陣列。
所以 slice 並不是簡單的傳值關係,就像指標和 chan 這些引用型別一樣,map 和 slice 對底層元素的修改都是引用型別的,map 和 slice 的頭部地址可以發生改變,但是他們引用到的底層陣列可能公共的。
3. 閉包 (closure)
需要注意的是,go 的閉包也是引用型別。考慮下面的程式碼:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
// Output: 4 3 2 1 0
}
fmt.Printf("\n")
for i := 0; i < 5; i++ {
defer func(){
fmt.Printf("%d ", i)
} () // Output: 5 5 5 5 5
}
第二個函式是外部變數在閉包內通過被改變了的情況,defer 函式內的表示式會在它出現的地方就已經被求值,然後在退出函式之前,按照 defer 出現的順序逆向執行。也就是說當第一條 defer 求值的時候,i=1,第五條 defer 求值的時候,i=5,對於兩個函式都是如此。區別在於第一個函式的 i 在每次 defer 是傳值進 printf 函式的,所以在 defer 中,i 等於有 5 份拷貝,而第二個函式使用閉包的方式引用了外部變數 i 其實只有一份!
要在閉包中避免上面的問題,可以有兩種方式。
// 方法1: 每次迴圈構造一個臨時變數 i
for i := 0; i < 5; i++ {
i := i
defer func(){ fmt.Printf("%d ", i) } ()
// Output: 4 3 2 1 0
}
// 方法2: 通過函式引數傳參
for i := 0; i < 5; i++ {
defer func(i int){ fmt.Printf("%d ", i) } (i)
// Output: 4 3 2 1 0
}
4. 深度拷貝
關於深度拷貝,這裡有個使用 gob 序列化反序列化的例子:
func deepCopy(dst, src interface{}) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
return err
}
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}
另外也有一些利用反射進行實現的方式:
5. 最佳實踐
slice 和 map 都不支援==操作符,判斷兩個 slice 相等需要自己寫迴圈判斷,這種迴圈判斷的方式效率並不會很低; map 不支援對元素取地址,如果這樣做,編輯器會拒絕編譯,原因是隨著 map 容量的擴張,底層資料結構可能改變,導致所取得的地址無效。 對 slice 的元素取地址編譯器是不禁止的,但是我們仍應該避免這樣做,因為 slice 擴張也會導致在新的記憶體空間重新構造底層陣列,而如果操作之前儲存的地址值可能會導致無法預料的結果。
for…range…
操作中,如果取了值,而不是通過去下標,像這樣:for i,v := range s
,其中 v 並不是 s 內元素的一個引用,改變 v 的值,並不能改變 s 中對應位置的元素。如果要這樣做必須通過下標 s[i] 進行操作。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- go slice深複製和淺複製Go
- 淺複製和深複製的概念與值複製和指標複製(引用複製)有關 淺複製 “指標複製 深複製 值複製指標
- go的深複製跟淺複製Go
- python 深複製和淺複製Python
- JavaScript 淺複製和深複製JavaScript
- JS物件複製:深複製和淺複製JS物件
- 淺談JS中物件的淺複製和深複製JS物件
- 淺複製與深複製
- python深複製和淺複製的區別Python
- js 淺拷貝(淺複製、淺克隆)、深拷貝(深複製、深克隆)JS
- Java引用複製、淺複製、深複製Java
- Go語言複製檔案Go
- C#中的物件深複製和淺複製C#物件
- 詳談Javascript中的深複製和淺複製JavaScript
- python 淺複製、深複製坑Python
- js 淺複製和深複製的區別和應用JS
- 對於複製普通物件 深複製和淺複製是否一樣物件
- python 的深淺複製Python
- 25. 深淺複製
- Java 中的深複製和淺複製你瞭解嗎?Java
- JavaScript中的淺複製與深複製JavaScript
- 檔案複製(Go語言實現)Go
- Python中的賦值與淺複製與深複製之間的關係Python賦值
- Python列表的深淺複製Python
- 深淺複製,溫故知新
- GO語言————7.5 切片的複製與追加Go
- JavaScript 深複製的迴圈引用問題JavaScript
- python 深/淺複製及其區別Python
- 聊一聊web前端那些事兒,關於深複製和淺複製Web前端
- go os.FileMode()傳值問題Go
- 淺顯直白的Python深複製與淺複製區別說明Python
- Day 7.5 資料型別總結 + 複製 淺複製 深複製資料型別
- Go語言引數傳遞是傳值?還是傳引用 ?Go
- 面試題分解—「淺複製/深複製、定義屬性使用copy還是strong ?」面試題
- 深入理解JavaScript之深淺複製JavaScript
- 資料共享(淺複製)與資料獨立(深複製)
- js 實現深複製/深複製JS
- go語言引數傳遞到底是傳值還是傳引用Go