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) }
不一定要用 slice of pointers?
從上面的例子中可以看到,如果會需要修改到
slice
中的元素,要避免參照到錯誤的
slice 或 slice 中的元素,使用 slice of pointers
可以算是最簡單的方式,但這麼做對效能和記憶體並不見得是最好的!一般來說使用
slice of structs 的效能比 slice of pointers 好(參考:Potential
dangers of storing structs rather than pointers)。
然而除了效能之外,可維護性也很重要,未來如果有需要修改元素內的資料時,是否有人會忘記把取出來的
slice 加上 &
,或者因為添加了 slice
中的元素而導致 slice relocate
而導致不必要的錯誤呢?
以我個人來說,如果這個 slice
或 slice
中的元素有可能會被操作修改,那麼就用
slice of pointers;如果這個 slice
單純只是拿來讀取,那麼就用 slice of
structs。
參考
- Potential dangers of storing structs rather than pointers @ medium replied:這篇說明使用 slice of structs 可能會碰到的問題和優點
- Bad Go: slices of pointers @ medium:這篇說明使用 slice of structs 的效能比較好