[掘竅] 使用 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)
}
不一定要用 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。
其他
slice of structs vs slice of pointers to structs
type Foo struct {}
sliceOfStructs := []Foo
sliceOfPointersOfStructs := []*Foo
在 slice 中使用 pointer 的好處在於未來如果有需要操作該 slice 中的 struct 時會方便許多(參考: Use Pointers in Golang Arrays),否則在該 slice 中的 struct 會比較麻煩。
使用 pointers to slices
Golang tips: why pointers to slices are useful and how ignoring them can lead to tricky bugs @ Medium
雖然我們知道 slice 本身就是 pass by reference,但為什麼我們經常會看到函式的參數是使用 pointers to slices 呢?
之所有會使用 pointer to slices,是因爲不使用 pointer to slices 的話,一旦 pointer 有改變(例如,copy
, assign
, append
),這些改變在函式外面將不會被看到。也就是說,如果這個 function 本身會改變 slice 的結構、size、記憶體位置的話,函式外的變數並不會有對應的改變,因為除了 slices 的值是 pass by reference 之外,用來描述這個 slices 的 metadata 都會是新的複製。
總結來說:如果你只是單純要修改元素的值,那麼可以 pass a slice by value;但若你可能會修改到 slice 的 length、position 等等,那就最好使用 pass a pointer to slice。
範例一
func main() {
slice := make([]string, 2, 3)
log(slice)
func(slice []string) {
// 先 append 的話 slice 因為 metadata 改變了,已經不同,
// 因此使用 slice[0], slice[1] 對於原本的 slice 進行改值將不會有作用
// slice = append(slice, "a")
slice[0] = "b"
slice[1] = "b"
// 後來才 append 的話,即可先修改到原本的 slice
// slice = append(slice, "a", "a")
log(slice)
}(slice)
log(slice)
}
func log(slice []string) {
fmt.Printf("%v ({len: %d, cap: %d}) \n", slice, len(slice), cap(slice))
fmt.Printf("addr of first element %p. add of slice %p\n", &slice[0], &slice)
}
範例二
func main() {
slice := make([]string, 1, 3)
func(slice []string) {
// 這個 slice 和原本的 slice 有不同的 metadata
// 因此無法使用 slice[0] 和 slice[1] 去改到外層的 slice
slice = slice[1:3]
slice[0] = "b"
slice[1] = "b"
log(slice)
}(slice)
log(slice)
}
參考
- Potential dangers of storing structs rather than pointers @ medium replied:這篇說明使用 slice of structs 可能會碰到的問題和優點
- Bad Go: slices of pointers @ medium:這篇說明使用 slice of structs 的效能比較好