go 切片 轉字串_Go 語言的指標切片
技術標籤:go 切片 轉字串
Go 讓操作 Slice 和其他基本資料結構成為一件很簡單的事情。對於來自 C/C++ 令人畏懼的指標世界的人來說,在大部分情況下使用 Golang 是一件令人幸福的事情。對於 JS/Python 的使用者來說,Golang 除了語法之外,沒有什麼區別。
然而,JS/Pyhon 的使用者或是 Go 的初學者總是遇到使用指標的時候。下面的場景就是他們可能會遇到的。
場景
假設這樣一個場景,你需要載入一個含有資料的字串指標的切片, []*string{}
。
讓我們看一段程式碼。
package main import ( "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("%sn", *n) } } // 原文章程式碼有 Bug ,譯者做了修改。
上面的示例程式碼生成了從 0 到 9 的數字。我們使用 strconv.Itoa
函式將每一個數字都轉換成對應的字串表達。然後將 #
字元新增至字串的頭部,最後利用 append
函式新增目標切片中。
執行上面的程式碼片段,你得到的輸出是
➜ sample go run main.go
#9
#9
#9
#9
#9
#9
#9
#9
#9
#9
這是什麼情況?
為什麼我只看到最後數字#9
被輸出??? 我非常確定我把其他的數字也加到了這個列表中!
讓我在這個示例程式中新增除錯程式碼。
package main import ( "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 slicen", numberString) // 將數字字串新增到切片中 listOfNumberStrings = append(listOfNumberStrings, &numberString) } for _, n := range listOfNumberStrings { fmt.Printf("%sn", *n) } }
調式程式碼的輸出為
➜ sample go run main.go Adding number #0 to the slice Adding number #1 to the slice Adding number #2 to the slice Adding number #3 to the slice Adding number #4 to the slice Adding number #5 to the slice Adding number #6 to the slice Adding number #7 to the slice Adding number #8 to the slice Adding number #9 to the slice
我看到他們被新增到...
這種事情怎麼發生到我頭上了?
[email protected]#! 啊啊啊啊啊!!
朋友,放輕鬆,讓我們看看到底發生了什麼。
var numberString string
numberString 在這裡會被分配到堆,讓我們假設,它的記憶體地址為 0x3AF1D234
。
for i := 0; i < 10; i++ {
numberString = fmt.Sprintf("#%s", strconv.Itoa(i))
listOfNumberStrings = append(listOfNumberStrings, &numberString)
}
現在讓我們從 0 迴圈至 9。
第一次迭代[i=0]
在這次迭代中,我們生成了字串 "#0"
並把它儲存到變數 numberString
。
接下來,我們獲取 numberString
變數的地址(&numberString
), 該地址為 0x3AF1D234
,然後把它新增到 listOfNumberStrings
的切片中。
listOfNumberStrings
現在應該像下圖一樣
第二次迭代[i=1]
我們重複以上步驟。
這一次,我們生成了字串 "#1"
,並把他儲存到相同的變數 numberString
中。
接下來,我們取 numberString
變數的地址(&numberString), 地址的值等於 0x3AF1D234
, 然後將其新增到 listOfNumberStrings
的切片中。
listOfNumberStrings
現在看起來應該像這樣:
希望現在已經開始讓你明白髮生什麼了。
這個切片目前有兩個變數。但是這兩個變數(下標為 1 和 下標為 2 ) 都儲存了相同的值: 0x3AF1D234
(numberString
的記憶體地址)。
然而,請記住,在第二次迭代的最後,儲存在 numberString
的字串是 "#1"
。
重複以上步驟直到迭代結束。
最後一次迭代的後,儲存在 numberString
的字串是 "#9"
。
現在讓我們看一下,當我們通過 *
操作符以解引用的方式, 嘗試輸出儲存在切片中的每一個元素的時候,會發生什麼?
for _, n := range listOfNumberStrings {
fmt.Printf("%sn", *n)
}
因為切片中儲存的每一個變數的值都是 0x3AF1D234
(像我們上面的例子中展示的),解引用該元素將返回存在該記憶體地址上的值。
從最後一個迭代,我們知道最後被儲存的值是 "#9"
, 因此輸出才像下面那樣。
➜ sample go run main.go
#9
#9
#9
#9
#9
#9
#9
#9
#9
#9
解決方案
有一個相當簡單的方法來解決這個問題:修改變數 numberString
宣告的位置。
package main
import (
"fmt"
"strconv"
)
func main() {
listOfNumberStrings := []*string{}
for i := 0; i < 10; i++ {
var numberString string
numberString = fmt.Sprintf("#%s", strconv.Itoa(i))
listOfNumberStrings = append(listOfNumberStrings, &numberString)
}
for _, n := range listOfNumberStrings {
fmt.Printf("%sn", *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++ 中奇妙的指標世界......老實說,你已經遇到了這個錯誤(並從中學習)!