介紹
Go的切片類型提供了一種方便有效的處理類型數(shù)據(jù)序列的方法。切片類似于其他語言中的數(shù)組,但具有一些不尋常的屬性。本文將介紹什么是切片并且如何使用他們
數(shù)組
切片類型是構(gòu)建在Go的數(shù)組類型之上的抽象,因此為了理解切片,我們必須首先理解數(shù)組。
數(shù)組類型定義指定長(zhǎng)度和元素類型。例如,類型[4]int表示四個(gè)整數(shù)的數(shù)組。數(shù)組的大小是固定的; 它的長(zhǎng)度是它的類型的一部分([4]int并且[5]int是不同的,不兼容的類型)。數(shù)組可以通常的方式編入索引,因此表達(dá)式s[n]從零開始訪問第n個(gè)元素。
package main
import "fmt"
func main() {
var a [4]int
a[0] = 1
fmt.Println(a[0], a[2])
}
output:
1 0
數(shù)組不需要顯式初始化; 數(shù)組的零值是一個(gè)現(xiàn)成的數(shù)組,其元素本身為零.
內(nèi)存中的表示[4]int只是順序排列的四個(gè)整數(shù)值:

Go的數(shù)組是值。數(shù)組變量表示整個(gè)數(shù)組; 它不是指向第一個(gè)數(shù)組元素的指針(如C中的情況)。這意味著當(dāng)您分配或傳遞數(shù)組值時(shí),您將復(fù)制其內(nèi)容。(為了避免復(fù)制,你可以傳遞一個(gè)指向數(shù)組的指針,但那是一個(gè)指向數(shù)組的指針,而不是一個(gè)數(shù)組。)一種思考數(shù)組的方法是作為一種結(jié)構(gòu),但有索引而不是命名字段:一個(gè)固定的-size復(fù)合值。
數(shù)組可以這么定義:
b := [2]string{"Penn", "Teller"}
你也可以這么定義:
b := [...]string{"Penn", "Teller"}
兩種定義方式變量都是[2]string類型
slices
數(shù)組有它們的位置,但它們有點(diǎn)不靈活,所以你不會(huì)在Go代碼中經(jīng)??吹剿鼈?。然而,切片無處不在。它們以陣列為基礎(chǔ),提供強(qiáng)大的功能和便利性。
切片的類型規(guī)范是[]T,切片T元素的類型。與數(shù)組類型不同,切片類型沒有指定的長(zhǎng)度。
切片文字聲明就像數(shù)組文字一樣,除了省略元素?cái)?shù):
可以使用調(diào)用的內(nèi)置函數(shù)make創(chuàng)建切片,
func make([] T,len,cap)[] T
其中T代表要?jiǎng)?chuàng)建的切片的元素類型。該make函數(shù)采用類型,長(zhǎng)度和可選容量。調(diào)用時(shí),make分配一個(gè)數(shù)組并返回引用該數(shù)組的切片。
var s [] byte
s = make([] byte,5,5)
// s == [] byte {0,0,0,0,0}
省略capacity參數(shù)時(shí),默認(rèn)為指定的長(zhǎng)度。這是相同代碼的更簡(jiǎn)潔版本:
s:= make([] byte,5)
可以使用內(nèi)置len和cap函數(shù)檢查切片的長(zhǎng)度和容量。
len(s)== 5
cap(s)== 5
切片長(zhǎng)度和容量的關(guān)系
還可以通過“切片”現(xiàn)有切片或陣列來形成切片。通過指定半開放范圍來完成切片,其中兩個(gè)索引用冒號(hào)分隔。例如,表達(dá)式b[1:4]創(chuàng)建一個(gè)包含元素1到3的b切片(結(jié)果切片的索引將為0到2)。
b:= [] byte {'g','o','l','a','n','g'}
// b [1:4] == [] byte {'o','l','a'},與b共享相同的存儲(chǔ)空間
切片表達(dá)式的開始和結(jié)束索引是可選的; 它們分別默認(rèn)為零和切片長(zhǎng)度:
// b [:2] == [] byte {'g','o'}
// b [2:] == [] byte {'l','a','n','g'}
// b [:] == b
這也是給定數(shù)組創(chuàng)建切片的語法:
x:= [3] string {“Лайка”,“Белка”,“Стрелка”}
s:= x [:] //引用x存儲(chǔ)的切片
切片內(nèi)部
切片是數(shù)組段的描述符。它由指向數(shù)組的指針,段的長(zhǎng)度及其容量(段的最大長(zhǎng)度)組成
[站外圖片上傳中...(image-2fae1-1563440373053)]
我們s之前創(chuàng)建的變量的make([]byte, 5)結(jié)構(gòu)如下:
[站外圖片上傳中...(image-2218ae-1563440373054)]
長(zhǎng)度是切片引用的元素?cái)?shù)。容量是底層數(shù)組中元素的數(shù)量(從切片指針引用的元素開始)。在我們通過接下來的幾個(gè)例子時(shí),將明確區(qū)分長(zhǎng)度和容量。
在切片時(shí)s,觀察切片數(shù)據(jù)結(jié)構(gòu)的變化及其與底層數(shù)組的關(guān)系:
s = s [2:4]
[站外圖片上傳中...(image-6f6534-1563440373054)]
切片不會(huì)復(fù)制切片的數(shù)據(jù)。它創(chuàng)建一個(gè)指向原始數(shù)組的新切片值。這使切片操作與操作數(shù)組索引一樣高效。因此,修改重新切片的元素(而不是切片本身)會(huì)修改原始切片的元素:
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
fmt.Println(e)
fmt.Println(d)
e[1] = 'm'
fmt.Println(e)
fmt.Println(d)
output:
[97 100]
[114 111 97 100]
[97 109]
[114 111 97 109]
切片不能超出其容量。嘗試這樣做會(huì)導(dǎo)致運(yùn)行時(shí)出現(xiàn)混亂,就像在切片或數(shù)組的邊界之外進(jìn)行索引一樣。類似地,切片不能在零以下重新切片以訪問數(shù)組中的早期元素
生長(zhǎng)切片(復(fù)制和追加功能)
要增加切片的容量,必須創(chuàng)建一個(gè)新的更大的切片并將原始切片的內(nèi)容復(fù)制到切片中
s := make([]byte, 1)
fmt.Println(s, len(s), cap(s))
t := make([]byte, len(s), (cap(s)+1)*2)
//for i := range s {
// t[i] = s[i]
//}
copy(t, s)
fmt.Println(t, len(t), cap(t))
output:
[0] 1 1
[0] 1 4
擴(kuò)容后,使用range賦值還是copy函數(shù)兩種方式是對(duì)等的
func copy(dst,src [] T)int
當(dāng)然copy方法簡(jiǎn)化了賦值的步驟
一個(gè)常用的操作是動(dòng)態(tài)增加容量,舉個(gè)例子:
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
}
func main() {
a := []byte{1, 2}
fmt.Println(a)
a = AppendByte(a, 3, 4, 6)
fmt.Println(a)
}
output:
[1 2]
[1 2 3 4 6]
上面的例子顯示如果容量不夠,以兩倍容量增加,當(dāng)然算法都是根據(jù)需求自定義
不過大多數(shù)情況不需要太復(fù)雜操作,可以通過append函數(shù)將新元素加入到slice尾部,如果slice容量不夠,系統(tǒng)會(huì)動(dòng)態(tài)添加容量
func append(s []T, x ...T) []T
舉例如下:
a := make([]int, 1)
fmt.Println(a, len(a), cap(a))
a = append(a, 1, 2, 3)
fmt.Println(a, len(a), cap(a))
output:
[0] 1 1
[0 1 2 3] 4 4
如果想通過append添加另一個(gè)slice,可以如下操作:
b := []string{"John", "Paul"}
fmt.Println(b, len(b), cap(b))
c := []string{"George", "Ringo", "Pete"}
b = append(b, c...)
fmt.Println(b, len(b), cap(b))
var d []string
b = append(b, d...)
fmt.Println(b, len(b), cap(b), d, d == nil)
output:
[John Paul] 2 2
[John Paul George Ringo Pete] 5 5
[John Paul George Ringo Pete] 5 5 [] true
nil slice也可以進(jìn)行append
一個(gè)可能的陷阱
如前所述,重新切片切片不會(huì)復(fù)制底層數(shù)組。完整數(shù)組將保留在內(nèi)存中,直到不再引用它為止。偶爾這會(huì)導(dǎo)致程序在只需要一小部分?jǐn)?shù)據(jù)時(shí)將所有數(shù)據(jù)保存在內(nèi)存中。
例如,此FindDigits函數(shù)將文件加載到內(nèi)存中,并在其中搜索第一組連續(xù)數(shù)字?jǐn)?shù)字,并將其作為新切片返回。
var digitRegexp = regexp.MustCompile(“[0-9] +”)
func FindDigits(filename string)[] byte {
b,_:= ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
此代碼的行為與廣告一樣,但返回的[]byte指向包含整個(gè)文件的數(shù)組。由于切片引用原始數(shù)組,只要切片保持在垃圾收集器周圍就不能釋放數(shù)組; 文件中幾個(gè)有用的字節(jié)將整個(gè)內(nèi)容保存在內(nèi)存中。
要解決此問題,可以在返回之前將數(shù)據(jù)復(fù)制到新切片:
func CopyDigits(filename string)[] byte {
b,_:= ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c:= make([] byte,len(b))
copy(c,b)
return c
}