[golang]切片詳解
1. 切片的定義
切片的結構定義在 reflect.SliceHeader
type SliceHeader struct{
Data uintptr
Len int
Cap int
}
看看切片的幾種定義方式:
var ( a []int // nil切片,和nil相等,一般用來表示一個不存在的切片 b = []int{} // 空切片,和nil不相等,一般用來表示一個空的集合 c = []int{1, 2, 3} // 有3個元素的切片,len=3,cap=3 d = c[:2] // 有2個元素的切片,len=2,cap=3 e = c[0:2:cap(c)] // 有2個元素的切片,len=2,cap=3 f = c[:0] // 有0個元素的切片,len=0,cap=3 g = make([]int, 3) // 有3個元素的切片,len=3,cap=3 h = make([]int, 2, 3) // 有2個元素的切片,len=2,cap=3 i = make([]int, 0, 3) // 有0個元素的切片,len=0,cap=3 )
2. 切片的記憶體
下面的TrimSpace()函式用於刪除[]byte中的空格。函式的實現利用了長度為0的切片的特性,實現簡單而高效:
func TrimSpace(s []byte) []byte { b := s[:0] for _, x := range s { if x != ' ' { b = append(b, x) } } return b }
其實類似的根據過濾條件原地修改切片元素的演算法都可以採用類似的處理方式,因為是刪除操作,所以不會出現記憶體不足的情況。
func Filter(s []byte, fn func(x byte) bool) []byte { b := s[:0] for _, x := range s { if !fn(x) { b = append(b, x) } } return b }
切片高效操作的要點是要降低記憶體分配的次數,儘量保證append()操作不會超出cap,降低觸發記憶體分配次數和每次分配記憶體的大小。
切片操作不會複製底層的陣列,底層陣列會被儲存在記憶體中,直到它不再被引用。但是有時候可能會因為一個小的記憶體引用而導致整個底層陣列屬於被使用的狀態,這時會延遲垃圾回收對底層陣列的回收。
例如下面這一段程式碼,FindPhoneNumber()函式載入整個檔案到記憶體中,然後搜尋第一個出現的電話號碼,最後結果以切片方式返回:
func FindPhoneNUmber(filename string) []byte { b, _ := ioutil.ReadFile(filename) return regexp.MustCompile("[0-9]+").Find(b) }
這段程式碼返回的[]byte指向儲存整個檔案的陣列。由於切片引用了整個原始陣列,導致垃圾回收不能及時釋放底層陣列的空間,一個小小的需求就可能系統需要長時間保留整個檔案資料。
要解決這個問題,通常需要將需要的資料複製到一個新的切片中,雖然值傳遞有一定的代價,但是切斷了對原始陣列的依賴
func FindPhoneNUmber(filename string) []byte { b, _ := ioutil.ReadFile(filename) b = regexp.MustCompile("[0-9]+").Find(b) return append([]byte{}, b...) }
類似的問題可能在刪除切片元素時會遇到。假設切片裡存放的是指標物件,那麼刪除末尾元素後,被刪除的元素依然會被切片底層陣列引用,導致不能夠被及時回收。
保險的方式是先講指向需要提前回收記憶體的指標設定為nil,保證垃圾回收器可以發現需要回收的物件,再進行切片的刪除:
var a []*int{...} a[len(a)-1] = nil a = a[:len(a)-1]
3. slice append的擴容
首先 Append 判斷型別是否 slice,然後呼叫 grow 擴容,從 l1 <= m 的判斷可以發現確實容量足夠的情況下,只是對原始陣列建立一個新的 slice
但當容量不足時,可以看到只有在當前元素 i0 小於1024時,才是按2倍速度正常,否則其實每次只增長25%。
其次,在擴容時,應該是按照2^n做一次記憶體向上取整的。