跳至主要内容

[掘竅] Go Slice

golang

使用 : 取出的會是 reference

參考:GO 程式設計模式:切片,介面,時間和性能

// 參考:https://coolshell.cn/articles/21128.html

func main() {
foo := make([]int, 5)
foo[3] = 42
foo[4] = 100

// 使用 `:` 時 foo 和 bar 仍會參照到底層相同的陣列(foo 和 bar 共享相同的記憶體)
// 因此修改 bar 即會修改到 foo
bar := foo[1:4]
bar[1] = 99
fmt.Println(bar) // [0 99 42]
fmt.Println(foo) // [0 0 99 42 100]
}

append 有機會產生指稱到不同的 slice

以同樣的例子來說,剛剛 foobar 指稱到底層相同的 array,但若這時後因為使用了 append 讓原本的 foo 因為 capacity 不夠而需要擴充時,這時候會產生 relocate 的情況,這時候將使得 append 後的 foo 和原本的 foo 指稱到的會是底層不同的記憶體位置:

func main() {
foo := make([]int, 5)
foo[3] = 42
foo[4] = 100

bar := foo[1:4]

// 當 foo 超過原本的 capacity 時,array 會 relocate
// 這個 foo 和原本 bar 切割出來的 foo 指稱到的已經是底層不同的 array
foo = append(foo, 0)

bar[1] = 99
fmt.Println(bar) // [0 99 42]
fmt.Println(foo) // [0 0 0 42 100 0]
}
warning

當 slice 在 append 時若 capacity 不足,會重新分配(relocate)記憶體,會有新的 array 被建立,以擴大 capacity,進而導致,append 前後的 slice 會指稱到底層不同陣列。

append 若沒有重新 relocate 還是會指稱到相同的位置

這是一個很神奇的例子:

// 程式來源:https://coolshell.cn/articles/21128.html
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path, '/')
fmt.Println(sepIndex) // 4

dir1 := path[:sepIndex] // len 4, cap: 14
dir2 := path[sepIndex+1:] // len 9, cap: 9

fmt.Println("dir1 =>", string(dir1), cap(dir1)) // AAAA
fmt.Println("dir2 =>", string(dir2), cap(dir2)) // BBBBBBBBB

dir1 = append(dir1, "suffix"...)
fmt.Println("dir1 =>", string(dir1)) // AAAAsuffix
fmt.Println("dir2 =>", string(dir2)) // uffixBBBB
}

從上面這段程式碼中可以看到,透過切割([low:high])的方式,產生了 dir1dir2,當我們使用切割時,被切割出來的 slice 預設的 capacity 會是從 low 開始到最後,也就是 cap(input) - low,因此 dir1 的 capacity 會是 14,dir2 的 capacity 會是 9。

這時候若針對 dir1 使用 append 且沒有超過其 capacity 時,dir1 和 dir2 實際上仍指稱到底層相同的 array,進而導致雖然是針對 dir1 使用 append,但卻同時改到了 dir2 的值:

        01234567890123
path AAAA/BBBBBBBBB
dir1 AAAA
dir2 BBBBBBBBB
--- append 後 ---
01234567890123
dir1 AAAAsuffix
dir2 uffixBBBB

要解決這個問題可以在切割時使用 Full slice expressions。full slice expression 指的是在切割時使用:

input[low:high:max]

以此方式切割出來的 slice 其 capacity 會是 max-low,因此當我們把 dir1 改成:

dir1 := path[:sepIndex:sepIndex]  // len: 4, cap: 4

由於 dir1 的 capacity 只有 4,因此在執行 append 時勢必會使得記憶體 relocate,最後 dir1dir2 就會指稱到底層不同的 array 而不會有互相影響的情況。

slice of pointers or slice of struct

參考 Golang - 使用 slice of pointers 或 slice of structs

奇怪的 slice

範例一

建立一個 toIntPointers 的方法,想要將所有 int 轉成 pointer:

func main() {
intValues := []int{1, 3, 5}
intPointers := toIntPointers(intValues)

fmt.Println(*intPointers[0], *intPointers[1], *intPointers[2]) // 5,5,5
}

使用 intPointers[i] = &v:預期輸出的結果是 1, 3, 5,但得到的是 5, 5, 5

// intPointers[i] = &v 會導致非預期的現象
func toIntPointers(intValues []int) []*int {
intPointers := make([]*int, len(intValues))
for i, v := range intValues {
fmt.Println("i, v", i, v)
intPointers[i] = &v
}

return intPointers
}

使用 intPointers[i] = &intValues[i] 才能得到預期結果:

// intPointers[i] = &v 會導致非預期的現象
func toIntPointers(intValues []int) []*int {
intPointers := make([]*int, len(intValues))
for i, v := range intValues {
fmt.Println("i, v", i, v)
intPointers[i] = &intValues[i]
}

return intPointers
}

之所以會這樣的原因在於,使用 for i, v := range intValues {...} 時,v 是一個變數,在每次迭代時會被賦予不同的值,但實際上參照的是同一個記憶體位置,所以如果使用 intPointers[i] = &v&v 塞到 intPointers 的話,都會塞進指稱到同一個記憶體位址的值,當迴圈跑到最後時這個 v 被賦值為 5,進而導致最終陣列中每一個元素的值都是 5。

golang_slice

參考資料