本文翻譯自Andrew Gerrand的博文 https://blog.golang.org/go-slices-usage-and-internals
前言
Go語言中提供了的切片類型琢感,方便使用者處理類型數(shù)據(jù)序列宙拉。
切片有點像其他語言中的數(shù)組喉镰,并且提供了一些額外的屬性。
數(shù)組
Go語言自帶了數(shù)組類型宣蔚,而切片類型是基于數(shù)組類型的抽象。因此脚囊,要理解切片類型郭怪,我們必須首先理解數(shù)組。
定義一個數(shù)組時广辰,需要指定數(shù)組長度和數(shù)組中元素的類型暇矫,比如說 [4]int
定義了長度為4的數(shù)組,其中的元素類型為int
择吊。一個數(shù)組的長度是固定的李根;長度是數(shù)組類型的一部分([4]int
和[5]int
就是兩個不同的類型)。數(shù)組以通常的方式進(jìn)行索引干发,所以表達(dá)式s[n]
能訪問到數(shù)組s
的第n個元素(從0開始)朱巨。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
在沒有顯式初始化時,數(shù)組默認(rèn)會將元素初始化為0枉长。
// a[2] == 0
在內(nèi)存中,[4]int
表示為順序排列的4個整數(shù)值
Go語言中的數(shù)組是一個值琼讽。數(shù)組變量表示整個數(shù)組,而不是指向數(shù)組第一個元素的指針(就像C語言那樣)钻蹬。這就意味著吼蚁,將一個數(shù)組當(dāng)作一個參數(shù)傳遞時,會完全拷貝數(shù)組中的內(nèi)容(如果不想完全拷貝數(shù)組问欠,可以傳一個指向數(shù)組的指針)肝匆。
可以把數(shù)組當(dāng)成這樣一種結(jié)構(gòu),它具有索引顺献,有著固定的大小旗国,可以用來存儲不同類型的元素。
一個字符串?dāng)?shù)組可以這樣定義
b := [2]string{"Penn", "Teller"}
或者讓編譯器來確定數(shù)組的長度
b := [...]string{"Penn", "Teller"}
上面的兩個例子中注整,b的類型都是 [2]string
能曾。
切片類型
數(shù)組類型是很有用的度硝,但是不太靈活,所以Go代碼中很少看到它們寿冕。但是切片類型卻是很常見的蕊程,因為它基于數(shù)組類型提供了強(qiáng)大的功能和開發(fā)便利。
切片類型的定義如[]T
驼唱,其中T
是切片中元素的類型藻茂。與數(shù)組類型不同,切片類型沒有固定的長度玫恳。
定義一個切片和定義一個數(shù)組的語法相似辨赐,唯一的不同是不需要定義切片長度。
letters := []string{"a", "b", "c", "d"}
可以用內(nèi)置的make
關(guān)鍵字定義一個切片
func make([]T, len, cap) []T
其中T
表示切片中元素的類型纽窟。make
函數(shù)接受元素類型肖油,長度和容量(可選)作為傳入?yún)?shù)。當(dāng)被調(diào)用時臂港,make
分配一個數(shù)組森枪,并且返回一個指向該數(shù)組的切片。
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
如果沒有傳入cap
參數(shù)审孽,它的默認(rèn)值是傳入的長度县袱。這是上面代碼的一個簡潔版本。
s := make([]byte, 5)
可以使用內(nèi)置的len
和cap
函數(shù)檢查切片的長度和容量佑力。
len(s) == 5
cap(s) == 5
下面兩個章節(jié)將討論長度和容量的關(guān)系式散。
切片的零值為nil
。對一個值為nil
的切片來說打颤,len
和cap
會返回0暴拄。
可以通過“切”一個數(shù)組或者是切片,來生成新的切片编饺。這個過程通過指定兩個索引的半開范圍來完成乖篷,兩個索引之間用冒號隔開。舉個例子透且,b[1:4]
會返回一個新的切片撕蔼,包含的元素為b
中的第1到第3的元素
b ;= []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'} 和b中的元素占用同一塊內(nèi)存
起始和結(jié)束索引是可選的,其默認(rèn)值分別為0和切片的長度
// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
基于數(shù)組創(chuàng)建切片語法與上面的類似秽誊。
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // s為指向x的引用
探尋切片內(nèi)部
切片是數(shù)組段的描述符鲸沮。它包含了一個指向數(shù)組的指針,數(shù)據(jù)段的長度和容量锅论。
通過s := make([]byte, 5)
方式聲明的切片結(jié)構(gòu)如下
長度是切片指向內(nèi)容中元素的個數(shù)讼溺。容量是底層數(shù)組中的元素個數(shù)(從切片指向的元素開始計數(shù))。長度和容量的區(qū)別會在下面的例子中解釋棍厌。
對s
進(jìn)行切片肾胯,觀察下面切片和數(shù)組的關(guān)系
s = s[2:4]
切片操作并不會拷貝s
中的數(shù)據(jù)竖席,而是創(chuàng)建一個新的切片指向原來的數(shù)組,這讓切片操作就像操作數(shù)組索引一樣高效敬肚。因此毕荐,對切片的元素進(jìn)行修改,會修改原始切片的元素艳馒。
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
進(jìn)行切片,其長度小于容量∨浚現(xiàn)在對其重新切片
s = s[:cap(s)]
切片的長度不能大于其容量第美。這樣做會導(dǎo)致一個runtime panic,就像對切片或者數(shù)組進(jìn)行越界訪問一樣陆爽。
增加切片容量
要增加切片的容量什往,必須新建一個容量更大的切片,然后將之前的切片的數(shù)據(jù)拷貝到新的切片中慌闭。這也是其他語言實現(xiàn)動態(tài)數(shù)組的方式别威。下面的例子,新建一個容量是s
兩倍的切片t
驴剔,然后將s
的數(shù)據(jù)拷貝到t
中省古,最后將t
賦值給s
:
t := make([]byte, len(s)m (cap(s)+1)*2) // +1對應(yīng) cap(s) == 0的情況
for i := range s {
t[i] = s[i]
}
s = t
使用內(nèi)置的copy
函數(shù)可以簡化上面的代碼。顧名思義丧失,copy
將數(shù)據(jù)從一個切片拷貝到另一個切片豺妓,并返回拷貝元素的數(shù)量。
語法如下:
func copy(dst, src []T) int
函數(shù)copy
支持兩個不同長度切片之間的拷貝布讹。另外琳拭,copy
可以處理源和目的切片指向相同底層數(shù)組的情況,正確處理重疊的切片描验。
簡化上面的代碼
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
一個常見的操作是在切片的末尾添加一個元素臀栈。下面的函數(shù)在一個切片的末尾增加一個元素,在容量不夠的情況下增加切片的容量挠乳,并且返回更新后的切片
func AppendByte(slice []byte, data ...type) []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
這樣的函數(shù)是很有用的,因為它能完全控制切片大小姑躲∷铮可以根據(jù)程序?qū)崿F(xiàn)的功能,分配更大黍析,更小的空間卖怜,或者為分配的空間設(shè)置一個上限。
但是大多數(shù)程序并不需要這樣的完全控制阐枣,這時候Go語言內(nèi)置的append
函數(shù)就派上用場了马靠。它的語法如下
fun append(s []T, x ...T) []T
函數(shù)append
將x
添加到s
末尾奄抽,如果需要就擴(kuò)展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)和長度為0的切片相似甩鳄,可以聲明一個切片變量逞度,然后在循環(huán)中在其末尾添加元素。
// 通過fn篩選出s中的元素
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
}
可能遇到的坑
如前面提到的妙啃,對一個切片進(jìn)行切片不會拷貝切片指向的數(shù)組档泽。這個數(shù)組會一致保存在內(nèi)存中,直到不再被引用揖赴。有時這樣會導(dǎo)致程序會將所有的數(shù)據(jù)保存在內(nèi)存中馆匿,即使只有一小部分?jǐn)?shù)據(jù)是被需要的。
舉個例子燥滑,下面FindDigits
函數(shù)會將一個文件中的內(nèi)容保存在內(nèi)存中渐北,搜索第一組連續(xù)數(shù)字,并將它們作為新的切片返回铭拧。
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
上面的代碼能完成所需要的功能赃蛛,但是返回的[]byte
切片指向的是保存了文件所有數(shù)據(jù)的數(shù)組。只要這個切片一直保留著羽历,垃圾回收將不能釋放保存了所有數(shù)據(jù)的數(shù)組焊虏。文件一小部分有用的數(shù)據(jù)將會讓所有的數(shù)據(jù)一直保存在內(nèi)存中。
要解決這個問題秕磷,可以先將有用的數(shù)據(jù)先保存到一個新的切片诵闭,然后返回新的切片。
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}