Golang語言 ---切片:用法和本質
- 原文: http://golang.org/doc/articles/slices_usage_and_internals.html
- 中文: http://zh-golang.appsp0t.com/doc/articles/slices_usage_and_internals.html
Go的切片型別為處理同類型資料序列提供一個方便而高效的方式。切片有些類似於其他語言中的陣列,但是有一些不同尋常的特性。本文將深入切片的本質,並講解它的用法。
陣列
Go的切片是在陣列之上的抽象資料型別,因此在瞭解切片之前必須要要理解陣列。
陣列型別由指定和長度和元素型別定義。例如,[4]int
型別表示一個四個整數的序列。陣列的長度是固定的,長度是陣列型別的一部分(int[4]
[5]int
是完全不同的型別)。陣列可以以常規的索引方式訪問,表示式 s[n]
訪問陣列的第 n
個元素。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
陣列不需要顯式的初始化;陣列元素會自動初始化為零值:
// a[2] == 0, the zero value of the int type
型別4int對應記憶體中四個連續的整數:
Go的陣列是值語義。一個數組變量表示整個陣列,它不是指向第一個元素的指標(比如C語言的陣列)。當一個數組變數被賦值或者被傳遞的時候,實際上會複製整個陣列。(為了避免複製陣列,你可以傳遞一個指向陣列的指標,但是陣列指標並不是陣列。)可以將陣列看作一個特殊的struct,結構的欄位名對應陣列的索引,同時成員的數目固定。
陣列的字面值想這樣:
b := [2]string{"Penn", "Teller"}
當然,也可以讓編譯器統計陣列字面值中元素的數目:
b := [...]string{"Penn", "Teller"}
這兩種寫法,b
都是對應 [2]string
型別。
切片
陣列雖然有適用它們的地方,但是陣列不夠靈活,因此在Go程式碼中陣列使用的並不多。但是,切片則使用得相當廣泛。切片基於陣列構建,但是提供更強的功能和便利。
切片的型別是 []T
,T
是切片元素的型別。和陣列不同的是,切片沒有固定的長度。
切片的字面值和陣列字面值很像,不過切片沒有指定元素個數:
letters := []string{"a", "b", "c", "d"}
切片可以內建函式 make
建立,函式簽名為:
func make([]T, len, cap) []T
T
代表被建立的切片元素的型別。函式 make
接受一個型別、一個長度和一個可選的容量引數。呼叫 make
時,內部會分配一個數組,然後返回陣列對應的切片。
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
當容量引數被忽略時,它預設為指定的長度。下面是簡潔的寫法:
s := make([]byte, 5)
可以使用內建函式 len
和 cap
獲取切片的長度和容量資訊。
len(s) == 5
cap(s) == 5
接下來的兩個小節將討論長度和容量之間的關係。
零值的切片型別變數為 nil
。對於零值切片變數,len
和 cap
都將返回 0
。
切片也可以基於現有的切片或陣列生成。切分的範圍由兩個由冒號分割的索引對應的半開區間指定。例如,表示式b[1:4]
建立的切片引用陣列 b
的第 1
到 3
個元素空間(對應切片的索引為0到2)。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
切片的開始和結束的索引都是可選的;它們分別預設為零和陣列的長度。
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
下面語法也是基於陣列建立一個切片:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x
切片的本質
一個切片是一個數組切割區間的描述。它包含了指向陣列的指標,切割區間的長度,和容量(切割區間的最大長度)。
前面使用 make([]byte, 5)
建立的切片變數s的結構如下:
長度是切片引用的元素數目。容量是底層陣列的元素數目(從切片指標開始)。關於長度和容量和區域將在下一個例子說明。
我們繼續對 s
進行切分,觀察切片的資料結構和它引用的底層陣列:
s = s[2:4]
切片並不複製整個切片元素。它建立一個新的切片執行同樣的底層陣列。這使得切片操作和陣列索引一樣高效。因此,通過一個新切片修改元素同樣會影響到原始的切片。
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}
前面建立的切片 s
長度小於它的容量。我們可以增長切片的長度為它的容量:
s = s[:cap(s)]
切片增長不能超出其容量。增長超出切片容量將會導致執行時異常,就像切片或陣列的索引超出範圍引起異常一樣。同樣,不能使用小於零的索引去訪問切片之前的元素。
切片生長(複製和追加)
要增加切片的容量必須建立一個新的、更大容量的切片,然後將原有切片的內容複製到新的切片。整個技術是一些支援動態陣列語言的常見實現。下面的例子將切片 s
容量翻倍,先建立一個2倍容量的新切片 t
,複製 s
的元素到 t
,然後將 t
賦值給 s
:
t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
迴圈中複製的操作可以由 copy
內建函式替代。copy
函式將源切片的元素複製到目的切片。它返回複製元素的數目。
func copy(dst, src []T) int
copy
函式支援不同長度的切片之間的複製(它只複製最小切片長度的元素)。此外,copy
函式可以正確處理源和目的切片有重疊的情況。
使用 copy
函式,我們可以簡化上面的程式碼片段:
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一個常見的操作是將資料追加到切片的尾部。下面的函式將元素追加到切片尾部,必要的話會增加切片的容量,最後返回更新的切片:
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
下面是 AppendByte
的一種用法:
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
類似 AppendByte
的函式比較實用,因為它提供了切片容量增長的完全控制。根據程式的特點,可能希望分配較小的活較大的塊,或則是超過某個大小再分配。
但大多數程式不需要完全的控制,因此Go提供了一個內建函式 append
,用於大多數場合;它的函式簽名:
func append(s []T, x ...T) []T
append函式將x追加到切片s的末尾,並且在必要的時候增加容量。
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
如果是要將一個切片追加到另一個切片尾部,需要使用…語法將第2個引數展開為引數列表。
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
可以宣告一個零值切片(nil
),然後在迴圈中向切片追加資料:
// Filter returns a new slice holding only
// the elements of s that satisfy f()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, i := range s {
if fn(i) {
p = append(p, i)
}
}
return p
}
可能的“陷阱”
正如前面所說,切片操作並不會複製底層的陣列。此層的陣列將被儲存在記憶體中,知道它不再被引用。有時候可能會因為一個小的記憶體引用導致儲存所有的資料。
例如,FindDigits
函式載入整個檔案到記憶體,然後搜尋第一個連續的數字,最後結果以切片方式返回。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
這段程式碼的行為和廣告類似,返回的 []byte
指向儲存整個檔案的陣列。因為切片引用了原始的陣列,導致GC不能釋放陣列的空間;一個小的需求導致需要儲存整個檔案。
要修復整個問題,可以將感興趣的資料複製到一個新的切片中:
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
可以使用 append
實現一個更簡潔的版本。這留給讀者作為練習。
進階閱讀
Effective Go 有關於切片和陣列的深入探討,並且Go 語言規範 定義了切片的相關、輔助、專有的函式。