1. 程式人生 > 其它 >go 切片 轉字串_Go 語言的指標切片

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

0bbfc2ac8de5c7e9e5f22d38ce3c7bb7.png
for i := 0; i < 10; i++ {
    numberString = fmt.Sprintf("#%s", strconv.Itoa(i))
    listOfNumberStrings = append(listOfNumberStrings, &numberString)
}

現在讓我們從 0 迴圈至 9。

第一次迭代[i=0]

在這次迭代中,我們生成了字串 "#0" 並把它儲存到變數 numberString

e5904c5152698eeb3f3acd5676c97bf7.png

接下來,我們獲取 numberString 變數的地址(&numberString), 該地址為 0x3AF1D234,然後把它新增到 listOfNumberStrings 的切片中。

listOfNumberStrings 現在應該像下圖一樣

f216ff354befa5489c42cd23d2d34e25.png

第二次迭代[i=1]

我們重複以上步驟。

這一次,我們生成了字串 "#1",並把他儲存到相同的變數 numberString 中。

ad6a4b094fb17732860fb8e59d5c09f7.png

接下來,我們取 numberString 變數的地址(&numberString), 地址的值等於 0x3AF1D234, 然後將其新增到 listOfNumberStrings 的切片中。

listOfNumberStrings 現在看起來應該像這樣:

623e81a2442f3288d41994114e9255eb.png

希望現在已經開始讓你明白髮生什麼了。

這個切片目前有兩個變數。但是這兩個變數(下標為 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++ 中奇妙的指標世界......老實說,你已經遇到了這個錯誤(並從中學習)!

f298cac877654ece5ecd38adb6271208.png