Go語言中的切片類型

圖文無關(guān)

本文翻譯自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ù)值

內(nèi)存中的 [4]int

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ù)組類型提供了強大的功能和開發(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)置的lencap函數(shù)檢查切片的長度和容量。

len(s) == 5
cap(s) == 5

下面兩個章節(jié)將討論長度和容量的關(guān)系。
切片的零值為nil。對一個值為nil的切片來說,lencap會返回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ù)段的長度和容量。

切片結(jié)構(gòu)

通過s := make([]byte, 5)方式聲明的切片結(jié)構(gòu)如下

s結(jié)構(gòu)

長度是切片指向內(nèi)容中元素的個數(shù)。容量是底層數(shù)組中的元素個數(shù)(從切片指向的元素開始計數(shù))。長度和容量的區(qū)別會在下面的例子中解釋。

s進(jìn)行切片,觀察下面切片和數(shù)組的關(guān)系

s = s[2:4]
切片和數(shù)組

切片操作并不會拷貝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)行切片,其長度小于容量?,F(xiàn)在對其重新切片

s = s[:cap(s)]
切片后結(jié)果

切片的長度不能大于其容量。這樣做會導(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ù)appendx添加到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
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 切片(slice)是 Golang 中一種比較特殊的數(shù)據(jù)結(jié)構(gòu),這種數(shù)據(jù)結(jié)構(gòu)更便于使用和管理數(shù)據(jù)集合。切片是圍繞動態(tài)...
    小孩真笨閱讀 1,214評論 0 1
  • 一、Go語言中切片類型出現(xiàn)的原因 切片是一種數(shù)據(jù)類型,這種數(shù)據(jù)類型便于使用和管理數(shù)據(jù)集合。創(chuàng)建一個100萬個int...
    碼墨閱讀 1,888評論 0 1
  • 最近面試較多,但其實很多內(nèi)容自己也不太會,所以有了自問自答的環(huán)節(jié)。a.什么是BFC浮動元素和絕對定位元素,非塊級盒...
    等花開_8e16閱讀 429評論 0 0
  • 你有沒有想過我也很好 只是你沒發(fā)現(xiàn)
    wsno閱讀 295評論 0 0
  • 今天關(guān)于身上的紅點,有了新的解釋,那就是被蟲子的體液毒到了。因為根據(jù)媽媽的理論,過敏的話應(yīng)該全身長紅點。
    木衛(wèi)33閱讀 141評論 0 0

友情鏈接更多精彩內(nèi)容