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做一次內存向上取整的。