![Image courtesy — https://xkcd.com/138/](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/1.png)Go 讓操作 Slice 和其他基本資料結構成為一件很簡單的事情。對於來自 C/C++ 令人畏懼的指標世界的人來說,在大部分情況下使用 Golang 是一件令人幸福的事情。對於 **JS/Python** 的使用者來說,Golang 除了文法之外,沒有什麼區別。然而,**JS/Pyhon** 的使用者或是 Go 的初學者總是遇到使用指標的時候。下面的情境就是他們可能會遇到的。## 情境假設這樣一個情境,你需要載入一個含有資料的字串指標的切片, `[]*string{}`。讓我們看一段代碼。```gopackage mainimport ("fmt""strconv")func main() {// 聲明一個字串指標的切片listOfNumberStrings := []*string{}// 預先聲明一個變數,這個變數會在添加將資料添加到切片之前儲存這個資料var numberString string// 從 0 到 9 的迴圈for i := 0; i < 10; i++ {// 在數字之前添加 `#`,構造一個字串numberString = fmt.Sprintf("#%s", strconv.Itoa(i))// 將數字字串添加到切片中listOfNumberStrings = append(listOfNumberStrings, &numberString)}for _, n := range listOfNumberStrings {fmt.Printf("%s\n", *n) }}// 原文章代碼有 Bug ,譯者做了修改。```上面的範例程式碼產生了從 0 到 9 的數字。我們使用 `strconv.Itoa` 函數將每一個數字都轉換成對應的字串表達。然後將 `#` 字元添加至字串的頭部,最後利用 `append` 函數添加目標切片中。運行上面的程式碼片段,你得到的輸出是```➜ sample go run main.go#9#9#9#9#9#9#9#9#9#9```> 這是什麼情況?>> 為什麼我只看到最後數字 `#9` 被輸出??? 我非常確定我把其他的數字也加到了這個列表中!>> 讓我在這個樣本程式中添加調試代碼。```gopackage mainimport ("fmt""strconv")func main() {// 聲明一個字串指標的切片listOfNumberStrings := []*string{}// 預先聲明一個變數,這個變數會在添加將資料添加到切片之前儲存這個資料var numberString string// 從 0 到 9 的迴圈for i := 0; i < 10; i++ {// 在數字之前添加 `#`,構造一個字串numberString = fmt.Sprintf("#%s", strconv.Itoa(i)) fmt.Printf("Adding number %s to the slice\n", numberString)// 將數字字串添加到切片中listOfNumberStrings = append(listOfNumberStrings, &numberString)}for _, n := range listOfNumberStrings {fmt.Printf("%s\n", *n)}}```調式代碼的輸出為```➜ sample go run main.goAdding number #0 to the sliceAdding number #1 to the sliceAdding number #2 to the sliceAdding number #3 to the sliceAdding number #4 to the sliceAdding number #5 to the sliceAdding number #6 to the sliceAdding number #7 to the sliceAdding number #8 to the sliceAdding number #9 to the slice```> 我看到他們被添加到...>> 這種事情怎麼發生到我頭上了?>> $@#! 啊啊啊啊啊!!朋友,放輕鬆,讓我們看看到底發生了什麼。```govar numberString string```numberString 在這裡會被分配到堆,讓我們假設,它的記憶體位址為 `0x3AF1D234`。![numberString on the stack at address 0x3AF1D234](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/2.png)```gofor i := 0; i < 10; i++ {numberString = fmt.Sprintf("#%s", strconv.Itoa(i))listOfNumberStrings = append(listOfNumberStrings, &numberString)}```現在讓我們從 0 迴圈至 9。### 第一次迭代[i=0]在這次迭代中,我們產生了字串 `"#0"` 並把它儲存到變數 `numberString`。![numberString stored at 0x3AF1D234 with content "#0"](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/3.png)接下來,我們擷取 `numberString` 變數的地址(`&numberString`), 該地址為 `0x3AF1D234`,然後把它添加到 `listOfNumberStrings` 的切片中。`listOfNumberStrings` 現在應該像一樣![listOfNumberStrings slice with value 0x3AF1D234](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/4.png)### 第二次迭代[i=1]我們重複以上步驟。這一次,我們產生了字串 `"#1"`,並把他儲存到相同的變數 `numberString` 中。![numberString stored at 0x3AF1D234 with content "#1"](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/5.png)接下來,我們取 `numberString` 變數的地址(&numberString), 地址的值等於 `0x3AF1D234`, 然後將其添加到 `listOfNumberStrings` 的切片中。`listOfNumberStrings` 現在看起來應該像這樣:![Two items in the slice BOTH with values 0x3AF1D234.](https://raw.githubusercontent.com/studygolang/gctt-images/master/uh-oh-is-in-go-slice-of-pointers/6.png)希望現在已經開始讓你明白髮生什麼了。這個切片目前有兩個變數。但是這兩個變數(下標為 1 和 下標為 2 ) 都儲存了相同的值: `0x3AF1D234` (`numberString` 的記憶體位址)。然而,請記住,在第二次迭代的最後,儲存在 `numberString` 的字串是 `"#1"`。重複以上步驟直到迭代結束。最後一次迭代的後,儲存在 `numberString` 的字串是 `"#9"`。現在讓我們看一下,當我們通過 `*` 操作符以解引用的方式, 嘗試輸出儲存在切片中的每一個元素的時候,會發生什嗎?```gofor _, n := range listOfNumberStrings { fmt.Printf("%s\n", *n)}```因為切片中儲存的每一個變數的值都是 `0x3AF1D234` (像我們上面的例子中展示的),解引用該元素將返回存在該記憶體位址上的值。從最後一個迭代,我們知道最後被儲存的值是 `"#9"`, 因此輸出才像下面那樣。```➜ sample go run main.go#9#9#9#9#9#9#9#9#9#9```## 解決方案有一個相當簡單的方法來解決這個問題:修改變數 `numberString` 聲明的位置。```gopackage mainimport ("fmt""strconv")func main() {listOfNumberStrings := []*string{}for i := 0; i < 10; i++ {var numberString stringnumberString = fmt.Sprintf("#%s", strconv.Itoa(i))listOfNumberStrings = append(listOfNumberStrings, &numberString)}for _, n := range listOfNumberStrings {fmt.Printf("%s\n", *n)}return}```我們在 `for` 迴圈中聲明這個變數。這是怎麼做到的? 每一次迴圈迭代,我們都強制重新在棧上聲明變數 `numberString` ,從而給他一個新的不同的記憶體位址。> **譯按**:這裡並非在棧上分配,通過逃逸分析 `go build -gcflags "-m"`,可以知道 `&numberString` 逃逸到堆上了。原作者這樣解釋,是因為作者使用 C 語言的角度去看待這個問題。在 C 語言中,也會遇到類似的問題,但的確可以通過在棧上強制申明一個新的變數來解釋上面的代碼。在 Golang 中,則通過編譯器的逃逸分析解釋以上代碼。然後我們用產生的字串更新變數,把它的地址添加到切片中。這樣的話,切片中的每一個元素都儲存著獨一無二的記憶體位址。上面的代碼的輸出將會是```➜ sample go run main.go#0#1#2#3#4#5#6#7#8#9```我希望這篇文章能夠協助到一些人。我起寫這篇文章的念頭是因為我與一名公司初級工程師的經曆,他遇到了相似的情境,並且完全繞不出來。這讓我想到了我掉進類似陷阱的情況,那時候我是一名前公司的 C 語言初級工程師。> Ps. 如果你來自 C/C++ 中奇妙的指標世界......老實說,你已經遇到了這個錯誤(並從中學習)! ;)
via: https://medium.com/@nitishmalhotra/uh-ohs-in-go-slice-of-pointers-c0a30669feee
作者:Nitish Malhotra 譯者:magichan 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
453 次點擊