你好,我是軒脈刃。這篇是關於go切片的一些問題和回答。
go的切片基本上是程式碼中使用最多的一種資料結構了,使用這種資料結構有哪些要注意的點,這個是非常必要了解的東西。基本上,以前寫的一篇部落格 https://www.cnblogs.com/yjf512/p/9531282.html 就說的很清楚了。這裡再深挖一些。
問題:go的切片資料結構是什麼樣子的?
切片是有可能在編譯器就被內聯的,而如果在編譯器沒有被內聯,進入執行期,就是直接使用SliceHeader資料結構。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
這三個欄位分別表示指標,長度,容量。
問題:為什麼在初始化slice的時候儘量補全cap
當我們要建立一個slice結構,並且往slice中append元素的時候,我們可能有兩種寫法來初始化這個slice。
方法1:
package main
import "fmt"
func main() {
arr := []int{}
arr = append(arr, 1,2,3,4, 5)
fmt.Println(arr)
}
方法2:
package main
import "fmt"
func main() {
arr := make([]int, 0, 5)
arr = append(arr, 1,2,3,4, 5)
fmt.Println(arr)
}
方法2相較於方法1,就只有一個區別:在初始化[]int slice的時候在make中設定了cap的長度,就是slice的大小。
這兩種方法對應的功能和輸出結果是沒有任何差別的,但是實際執行的時候,方法2會比少執行了一個growslice的命令。
這個我們可以通過列印彙編碼進行檢視:
方法1:
方法2:
我們看到方法1中使用了growsslice方法,而方法2中是沒有呼叫這個方法的。
這個growslice的作用就是擴充slice的容量大小。就好比是原先我們沒有定製容量,系統給了我們一個能裝兩個鞋子的盒子,但是當我們裝到第三個鞋子的時候,這個盒子就不夠了,我們就要換一個盒子,而換這個盒子,我們勢必還需要將原先的盒子裡面的鞋子也拿出來放到新的盒子裡面。所以這個growsslice的操作是一個比較複雜的操作,它的表現和複雜度會高於最基本的初始化make方法。對追求效能的程式來說,應該能避免儘量避免。
具體對growsslice函式具體實現同學有興趣的可以參考原始碼src的 runtime/slice.go 。
當然,我們並不是每次都能在slice初始化的時候就能準確預估到最終的使用容量的。所以這裡使用了一個“儘量”。明白是否設定slice容量的區別,我們在能預估容量的時候,請儘量使用方法2那種預估容量後的slice初始化方式。
問題:如果不設定cap,make slice的時候,建立的cap為多大?
如果不設定cap,不管是使用make,還是直接使用[]slice 進行初始化,編譯器都會計算初始化所需的空間,使用最小化的cap進行初始化。
a := make([]int, 0) // cap 為0
a := []int{1,2,3} // cap 為3
可以從ssa看出
問題:slice什麼時候決定擴張?
之前寫過一篇文章 https://www.cnblogs.com/yjf512/p/10714792.html 裡面得出的結論就是slice在編譯期就決定是否要呼叫growslice。
這個邏輯是正確的。
編譯器在ssa的時候 對於append是會轉換為 OAPPEND(cmd/compile/internal/typecheck/universe.go) 。而在 cmd/compile/internal/ssagen/ssa.go 中,對其進行判斷。
目前還看不懂下面append下面的邏輯,不過基於這個註釋,能瞭解到這裡growslice的邏輯。比較擴容前後大小,如果原先cap小於擴容後需要cap,就growslice。
總結
琢磨了四個關於切片的問題:
問題:go的切片資料結構是什麼樣子的?
問題:為什麼在初始化slice的時候儘量補全cap?
問題:如果不設定cap,make slice的時候,建立的cap為多大?
問題:slice什麼時候決定擴張?