[掘竅] Go Slice
使用 :
取出的會是 reference
// 參考: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
以同樣的例子來說,剛剛 foo
和 bar
指稱到底層相同的 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]
}
當 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]
)的方式,產生了 dir1
和 dir2
,當我們使用切割時,被切割出來的 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,最後 dir1
和 dir2
就會指稱到底層不同的 array 而不會有互相影響的情況。
slice of pointers or slice of struct
奇怪的 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。