- 原文標題:Go Slices: usage and internals
- 原文做者:Andrew Gerrand
- 原文時間:2011-01-05
Go 的切片(slice
)提供了一種方便、高效的處理特定類型數據序列的方法。切片相似於其餘語言中的數組,但有些特別的地方。本文討論切片是什麼、以及如何使用它。html
Go 中切片是基於數組的,所以爲了理解切片,首先得理解數組。
一個數組類型包括元素類型和元素個數。例如,類型 [4]int
表示有 4 個整數元素的數組。一個數組的長度是固定的,長度自己也是類型的一部分([4]int
和 [5]int
是兩個不一樣的類型)。數組能經過下標訪問,所以表達式 s[n]
表示訪問下標從 0 開始的第 n 個元素。git
var a [4]int
a[0] = 1
i := a[0]
// i == 1
複製代碼
數組不須要顯示初始化,它會自動初始化爲數組零值(the zero value of an array),這個零值中的全部的元素的值都是該元素類型的零值:golang
// a[2] == 0, 初始化爲 int 類型的零值
複製代碼
類型 [4]int
的內存表示就是 4 個整數順序擺放: 數組
struct
,經過下標來使用,而不是成員名,這是一種固定大小的複合值。
literal
)能這樣寫:
b := [2]string{"Penn", "Teller"}
複製代碼
或者,讓編譯器計算元素的個數app
b := [...]string{"Penn", "Teller"}
複製代碼
上面兩種狀況,變量 b
的類型都是 [2]string
。函數
數組用本身的用武之地,但不靈活,因此並不常常在 Go 代碼中出現。與它對比,切片就經常使用的多。它基於數組,但很是方便和強大。
切片類型表示爲 []T
,其中 T
是切片中元素的類型。與數組類型不一樣,切片類型不用指定長度。
一個切片的字面量和數組相似,除了不能指定元素個數:ui
letters := []string{"a", "b", "c", "d"}
複製代碼
可使用內建函數 make
來建立切片,make
函數簽名以下:spa
func make([]T, len, cap) []T 複製代碼
其中 T
表示新切片中元素的類型。make
參數有:切片元素類型、切片長度、切片容量(可選)。調用 make
時,它會申請一個數組,而後返回一個使用該數組的切片。3d
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0}
複製代碼
若是不指定切片容量,默認爲與切片長度同樣大小。下面是一個更簡單的版本:指針
s := make([]byte, 5)
複製代碼
使用內建函數 len
和 cap
來獲取切片的長度和容量。
len(s) == 5
cap(s) == 5
複製代碼
接下來兩部分討論切片長度和切片容量的關係。
切片的零值爲 nil
,這種狀況下 len
和 cap
都返回 0。
對一個已有的數組或切片進行切片操做(譯者:注意切片
和切片操做
的區別),能生成一個新的切片。切片操做經過兩個下標中間加個冒號來指定一個半開的區間。好比,表達式 b[1:4]
建立了一個新切片,新切片包括 b
中下標爲 一、二、3 的元素(新切片中對應的下標爲0、一、2)。
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, 與b共用同一內存空間
複製代碼
表達式中開始和結束的下標都是可選的,默認分別爲 0 和切片的長度。
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
複製代碼
下面是經過數組來建立切片:
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 切片 s 使用數組 x 的內存空間
複製代碼
切片是數組某段的描述符,包括一個指向數組的指針
,當前段的長度(length)
,還有容量(capacity)
(這個段能到達的最大長度)。
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
的長度,使得長度和容量相等。
s = s[:cap(s)]
複製代碼
想要增長切片的容量,只能建立一個新的,容量更大的切片,而後把原始數據複製過去。其餘語言的動態數組的實現也是使用的這種幕後技術。下面代碼的操做:建立一個容量爲 s
的兩倍的新切片 t
,複製 s
的內容到 t
,最後將 t
賦值給 s
。
t := make([]byte, len(s), (cap(s)+1)*2) // +1 防止 cap(s) == 0 的狀況
for i := range s {
t[i] = s[i]
}
s = t
複製代碼
內建函數 copy
實現了上面代碼中循環的功能。它將要複製的內容從原始切片拷貝到目的切片,返回拷貝元素的個數。
func copy(dst, src []T) int 複製代碼
copy
支持不一樣長度的切片間的拷貝(拷貝長度爲兩切片中長度小的那個)。並且,它還能正確處理源切片和目的切片處於同一個數組上的狀況。
使用 copy
上面的代碼簡化爲:
t = make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
複製代碼
將數據添加到切片的末尾是很經常使用的操做。下面這個函數會將一個 byte
元素添加到元素類型爲 byte
的切片中。若是有必要,它會增長切片的大小。最後返回添加後的切片。
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { //從新申請空間
// 申請兩倍的空間,以備後用
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 提供了一個內建函數實現這個功能。太部分狀況下都很好用,函數簽名以下:
func append(s []T, X ...T) []T 複製代碼
apppend
將元素 x
添加到切片 s
的末尾,若有必要,它會增長切片的容量。
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
複製代碼
將一個切片添加到另外一個切片,使用操做符 ...
將第二切片展開成參數列表。
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "pete"}
a = append(a, b...) // 等同於 」append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
複製代碼
由於切片的零值(nil
)有相似於長度爲零的切片的屬性,所以能夠直接聲明一個變量,向其添加元素:
// Filter 函數返回一個包含知足 fn() 的元素的新切片
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
複製代碼
前面提到,對切片進行切片操做不會複製切片結構裏面的數組數據。整個數組會一直佔用內存空間,直到引用數爲零。有些狀況下,這會形成一個問題:程序只須要用到一塊數據中一小段,卻得把整個數據塊保留在內存中。
例如,下面的函數加載一個文件進內存,查找第一組連續數字的序列做爲新的切片返回。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
複製代碼
這段代碼能夠正確運行,可是有個問題:返回的切片使用了包含整個文件的數組。由於返回的切片引用了原始數組,只要切片還在,原始數組就不能被垃圾回收 -- 對文件某一小段的使用使得整個文件都必須佔用內存。
爲了解決這個問題,能夠在返回以前將目標數據複製到一個新切片中:
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 language specification 定義了切片以及相關的輔助函數。
(原文完)
只用切片不用數組 : )