[掘竅] 使用 slice of pointers 或 slice of structs
TL;DR
如果這個 slice 或 slice 中的元素有可能會被操作修改,那麼就用 slice of pointers;如果這個 slice 單純只是拿來讀取,那麼就用 slice of structs;或者也可以無腦的使用 slice of pointers 以避免後續不小心而產生的 bug。
為什麼會看到 slice of pointers 的用法
在 Golang 中常會看到 slice 中存放的是 slice of pointers,使用 slice of pointers 或 slice of structs 有幾個很重要的差別。先來看看下面的程式碼。
假設我們先定義一個名為 Vertex
的 struct:
type Vertex struct {
X int
Y int
}
接者在建立 slice of struct Vertex:
- 當我們把「slice 中的 struct」取出來的時候,實際上是把
slice[1]
複製一份到point1
裡面,因此即使修改了 point1 內的 X 和 Y,在輸出 slice 時並不會看到改變
func main() {
var slice []Vertex
for i := 0; i < 4; i++ {
slice = append(slice, Vertex{i * 10, i * 100})
}
// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("before change: %+v \n", slice)
point1 := slice[1] // 取出的是 slice of struct
point1.X += 1
point1.Y += 1
// slice[1] 並沒有被修改: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("after change: %+v \n", slice)
}
- 但若我們取出來的是「slice of pointer」,這時候才會參照回原本 slice 中的 struct,因此當我們修改
point1
內的 X 和 Y 時,會看到輸出的 slice 產生對應的改變
func main() {
var slice []Vertex
for i := 0; i < 4; i++ {
slice = append(slice, Vertex{i * 10, i * 100})
}
// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("before change: %+v \n", slice)
point1 := &slice[1] // 差別在這:取出的是 slice of pointer
point1.X += 1
point1.Y += 1
// slice[1] 有被修改: [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("after change: %+v \n", slice)
}
- 除此之外,當 slice 因為長度改變時,會產生「記憶體重新配置(memory relocate)」,這時候的 slice 和原本一開始的 slice 已經是指稱到底層不同的 array,進而導致修改
point2
內的 X, Y 後,在最終 輸出的 slice 並無法看到改變
func main() {
var slice []Vertex
for i := 0; i < 4; i++ {
slice = append(slice, Vertex{i * 10, i * 100})
}
// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("before change: %+v \n", slice)
point1 := &slice[1]
point2 := &slice[2]
point1.X += 1
point1.Y += 1
// slice[1] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("after change: %+v \n", slice)
// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
slice = append(slice, Vertex{4, 40})
// 這時候對 point2 修改無法修改到原本 slice 中的 {X:2, Y:20}
point2.X += 1
point2.Y += 1
// slice[2] 並沒有被修改: [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300} {X:4 Y:40}]
fmt.Printf("after reallocate: %+v \n", slice)
}
解決參照到不同 slice 的作法
要解決這樣的問題,有幾種不同的做法:
方法一:避免 slice 超過其 cap 導致重新 relocate 外
可以的話,在一開始定義 slice 時,就把 slice 的 length 設好,且避免 relocate:
func main() {
// 方法一:先把 slice 的 len 設好,並避免 relocate
slice := make([]Vertex, 8)
for i := 0; i < 4; i++ {
slice[i] = Vertex{i * 10, i * 100}
}
// ...
}
方法二:使用 slice of pointers
// 使用 2. slice of pointers
func main() {
var slice []*Vertex // 這裡使用 slice of pointers
for i := 0; i < 4; i++ {
slice = append(slice, &Vertex{i * 10, i * 100})
}
printAll("before change", slice) // [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
point1 := slice[1]
point2 := slice[2]
point1.X += 1
point1.Y += 1
printAll("after change", slice) // [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
slice = append(slice, &Vertex{4, 40})
point2.X += 1
point2.Y += 1
// 透過 slice 可以看到修改後的結果
printAll("after reallocate", slice) // [{X:0 Y:0} {X:11 Y:101} {X:21 Y:201} {X:30 Y:300} {X:4 Y:40}]
}
func printAll(description string, slice []*Vertex) {
sliceStruct := make([]Vertex, len(slice))
for i, s := range slice {
sliceStruct[i] = *s
}
fmt.Printf("%s: %+v \n", description, sliceStruct)
}
方法三:使用 index 而非使用 Pointer
func main() {
var slice []Vertex
for i := 0; i < 4; i++ {
slice = append(slice, Vertex{i * 10, i * 100})
}
// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("before change: %+v \n", slice)
slice[1].X += 1
slice[1].Y += 1
// slice[1] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
fmt.Printf("after change: %+v \n", slice)
// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
slice = append(slice, Vertex{4, 40})
// 透過 index 的方式修改 slice
slice[2].X += 1
slice[2].Y += 1
// slice[2] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:21 Y:201} {X:30 Y:300} {X:4 Y:40}]
fmt.Printf("after reallocate: %+v \n", slice)
}