天天看點

Go基礎系列:Go slice分片切片詳解

slice表示切片(分片),例如對一個數組進行切片,取出數組中的一部分值。在現代程式設計語言中,slice(切片)幾乎成為一種必備特性,它可以從一個數組(清單)中取出任意長度的子數組(清單),為操作資料結構帶來非常大的便利性,如python、perl等都支援對數組的slice操作,甚至perl還支援對hash資料結構的slice。

但Go中的slice和這些語言的slice不太一樣,前面所說的語言中,slice是一種切片的操作,切片後傳回一個新的資料對象。而Go中的slice不僅僅是一種切片動作,還是一種資料結構(就像數組一樣)。

slice的存儲結構

Go中的slice依賴于數組,它的底層就是數組,是以數組具有的優點,slice都有。且slice支援可以通過append向slice中追加元素,長度不夠時會動态擴充,通過再次slice切片,可以得到得到更小的slice結構,可以疊代、周遊等。

實際上slice是這樣的結構:先建立一個有特定長度和資料類型的底層數組,然後從這個底層數組中選取一部分元素,傳回這些元素組成的集合(或容器),并将slice指向集合中的第一個元素。換句話說,slice自身維護了一個指針屬性,指向它底層數組中的某些元素的集合。

例如,初始化一個slice資料結構:

slice := make([]int, 3,5) // 輸出slice fmt.Println(my_slice) // 輸出:[0 0 0]

這表示先聲明一個長度為5、資料類型為int的底層數組,然後從這個底層數組中從前向後取3個元素(即index從0到2)作為slice的結構。

如下圖:

每一個slice結構都由3部分組成:容量(capacity)、長度(length)和指向底層數組某元素的指針,它們各占8位元組(1個機器字長,64位機器上一個機器字長為64bit,共8位元組大小,32位架構則是32bit,占用4位元組),是以任何一個slice都是24位元組(3個機器字長)。

  • Pointer:表示該slice結構從底層數組的哪一個元素開始,該指針指向該元素
  • Capacity:即底層數組的長度,表示這個slice目前最多能擴充到這麼長
  • Length:表示slice目前的長度,如果追加元素,長度不夠時會擴充,最大擴充到Capacity的長度(不完全準确,後面數組自動擴充時解釋),是以Length必須不能比Capacity更大,否則會報錯

對上面建立的slice來說,它的長度為3,容量為5,指針指向底層數組的index=0。

可以通過len()函數擷取slice的長度,通過cap()函數擷取slice的Capacity。

1
2
3
4
              my_slice := make([]int,3,5)

fmt.Println(len(my_slice))  // 3
fmt.Println(cap(my_slice))  // 5
           

還可以直接通過print()或println()函數去輸出slice,它将得到這個slice結構的屬性值,也就是length、capacity和pointer:

1
2
              my_slice := make([]int,3,5)
println(my_slice)      // [3/5]0xc42003df10
           

[3/5]

表示length和capacity,

0xc42003df10

表示指向的底層數組元素的指針。

務必記住slice的本質是

[x/y]0xADDR

,記住它将在很多地方有助于了解slice的特性。另外,個人建議,雖然slice的本質不是指針,但仍然可以将它看作是一種包含了另外兩種屬性的不純粹的指針,也就是說,直接認為它是指針。其實不僅slice如此,map也如此。

建立、初始化、通路slice

有幾種建立slice資料結構的方式。

一種是使用make():

1
2
3
4
5
              // 建立一個length和capacity都等于5的slice
slice := make([]int,5)

// length=3,capacity=5的slice
slice := make([]int,3,5)
           

make()比new()函數多一些操作,new()函數隻會進行記憶體配置設定并做預設的賦0初始化,而make()可以先為底層數組配置設定好記憶體,然後從這個底層數組中再額外生成一個slice并初始化。另外,make隻能建構slice、map和channel這3種結構的資料對象,因為它們都指向底層資料結構,都需要先為底層資料結構配置設定好記憶體并初始化。

還可以直接指派初始化的方式建立slice:

1
2
3
4
5
              // 建立長度和容量都為4的slice,并初始化指派
color_slice := []string{"red","blue","black","green"}

// 建立長度和容量為100的slice,并為第100個元素指派為3
slice := []int{99:3}
           

注意區分array和slice:

1
2
3
4
5
              // 建立長度為3的int數組
array := [3]int{10, 20, 30}

// 建立長度和容量都為3的slice
slice := []int{10, 20, 30}
           

由于slice底層是數組,是以可以使用索引的方式通路slice,或修改slice中元素的值:

1
2
3
4
5
6
7
8
              // 建立長度為5、容量為5的slice
my_slice := []int{11,22,33,44,55}

// 通路slice的第2個元素
print(my_slice[1])

// 修改slice的第3個元素的值
my_slice[2] = 333
           

由于slice的底層是數組,是以通路

my_slice[1]

實際上是在通路它的底層數組的對應元素。slice能被通路的元素隻有length範圍内的元素,那些在length之外,但在capacity之内的元素暫時還不屬于slice,隻有在slice被擴充時(見下文append),capacity中的元素才被納入length,才能被通路。

nil slice和空slice

當聲明一個slice,但不做初始化的時候,這個slice就是一個nil slice。

1
2
              // 聲明一個nil slice
var nil_slice []int
           

nil slice表示它的指針為nil,也就是這個slice不會指向哪個底層數組。也是以,nil slice的長度和容量都為0。

1
2
3
4
              |--------|---------|----------|
|  nil   |   0     |     0    |
|  ptr   | Length  | Capacity |
|--------|---------|----------|
           

還可以建立空slice(Empty Slice),空slice表示長度為0,容量為0,但卻有指向的slice,隻不過指向的底層數組暫時是長度為0的空數組。

1
2
3
4
5
              // 使用make建立
empty_slice := make([]int,0)

// 直接建立
empty_slice := []int{}
           

Empty Slice的結構如下:

1
2
3
4
              |--------|---------|----------|
|  ADDR  |   0     |     0    |
|  ptr   | Length  | Capacity |
|--------|---------|----------|
           

雖然nil slice和Empty slice的長度和容量都為0,輸出時的結果都是

[]

,且都不存儲任何資料,但它們是不同的。nil slice不會指向底層數組,而空slice會指向底層數組,隻不過這個底層數組暫時是空數組。

可以使用println()來輸出驗證:

1
2
3
4
5
6
7
8
              package main

func main() {
    var nil_s []int
    empty_s:= []int{}
    println(nil_s)
    println(empty_s)
}
           

輸出結果:

1
2
              [0/0]0x0
[0/0]0xc042085f50
           

當然,無論是nil slice還是empty slice,都可以對它們進行操作,如append()函數、len()函數和cap()函數。

對slice進行切片

可以從slice中繼續切片生成一個新的slice,這樣能實作slice的縮減。

對slice切片的文法為:

1
2
              SLICE[A:B]
SLICE[A:B:C]
           

其中A表示從SLICE的第幾個元素開始切,B控制切片的長度(B-A),C控制切片的容量(C-A),如果沒有給定C,則表示切到底層數組的最尾部。

還有幾種簡化形式:

1
2
3
              SLICE[A:]  // 從A切到最尾部
SLICE[:B]  // 從最開頭切到B(不包含B)
SLICE[:]   // 從頭切到尾,等價于複制整個SLICE
           

例如:

1
2
3
4
              my_slice := []int{11,22,33,44,55}

// 生成新的slice,從第二個元素取,切取的長度為2
new_slice := my_slice[1:3]
           

注意,截取時"左閉右開"。是以上面

new_slice

是從

my_slice

的index=1開始截取,截取到index=3為止,但不包括index=3這個元素。是以,新的slice是由

my_slice

中的第2個元素、第3個元素組成的新的資料結構,長度為2。

以下是slice切片生成新的slice後的結構:

不難發現,一個底層數組,可以生成無數個slice,且對于new_slice而言,它并不知道底層數組index=0的那個元素。

還可以控制切片時新slice的容量:

1
2
3
4
              my_slice := []int{11,22,33,44,55}

// 從第二個元素取,切取的長度為2,容量也為2
new_slice := my_slice[1:3:3]
           

這時新slice的length等于capacity,底層數組的index=4、5将對new_slice永不可見,即使後面對new_slice進行append()導緻底層數組擴容也仍然不可見。具體見下文。

由于多個slice共享同一個底層數組,是以當修改了某個slice中的元素時,其它包含該元素的slice也會随之改變,因為slice隻是一個指向底層數組的指針(隻不過這個指針不純粹,多了兩個額外的屬性length和capacity),實際上修改的是底層數組的值,而底層數組是被共享的。

當同一個底層數組有很多slice的時候,一切将變得混亂不堪,因為我們不可能記住誰在共享它,通過修改某個slice的元素時,将也會影響那些可能我們不想影響的slice。是以,需要一種特性,保證各個slice的底層數組互不影響,相關内容見下面的"擴容"。

copy()函數

可以将一個slice拷貝到另一個slice中。

1
2
              $ go doc builtin copy
func copy(dst, src []Type) int
           

這表示将src slice拷貝到dst slice,src比dst長,就截斷,src比dst短,則隻拷貝src那部分。

copy的傳回值是拷貝成功的元素數量,是以也就是src slice或dst slice中最小的那個長度。

例如:

1
2
3
4
5
6
7
8
9
10
              s1 := []int{11, 22, 33}
s2 := make([]int, 5)
s3 := make([]int,2)

num := copy(s2, s1)
copy(s3,s1)

fmt.Println(num)   // 3
fmt.Println(s2)    // [11,22,33,0,0]
fmt.Println(s3)    // [11,22]
           

此外,copy還能将字元串拷貝到byte slice中,因為字元串實際上就是

[]byte

1
2
3
4
5
6
7
              func main() {
	s1 := []byte("Hello")
	num := copy(s1, "World")
	fmt.Println(num)
	fmt.Println(s1)    // 輸出[87 111 114 108 100 32]
	fmt.Println(string(s1))  //輸出"World"
}
           

append()函數

可以使用append()函數對slice進行擴充,因為它追加元素到slice中,是以一定會增加slice的長度。

但必須注意,append()的結果必須被使用。所謂被使用,可以将其輸出、可以指派給某個slice。如果将append()放在空上下文将會報錯:append()已評估,但未使用。同時這也說明,append()傳回一個新的slice,原始的slice會保留不變。

例如:

1
2
3
4
5
              my_slice := []int{11,22,33,44,55}
new_slice := my_slice[1:3]

// append()追加一個元素2323,傳回新的slice
app_slice := append(new_slice,2323)
           

上面的append()在

new_slice

的後面增加了一個元素2323,是以

app_slice[2]=2323

。但因為這些slice共享同一個底層數組,是以2323也會反映到其它slice中。

現在的資料結構圖如下:

當然,如果append()的結果重新指派給new_slice,則

new_slice

會增加長度。

同樣,由于string的本質是[]byte,是以string可以append到byte slice中:

1
2
3
              s1 := []byte("Hello")
s2 := append(s1, "World"...)
fmt.Println(string(s2))   // 輸出:HelloWorld
           

擴容

當slice的length已經等于capacity的時候,再使用append()給slice追加元素,會自動擴充底層數組的長度。

底層數組擴充時,會生成一個新的底層數組。是以舊底層數組仍然會被舊slice引用,新slice和舊slice不再共享同一個底層數組。

1
2
3
4
5
6
7
8
9
10
11
12
              func main() {
    my_slice := []int{11,22,33,44,55}
    new_slice := append(my_slice,66)

    my_slice[3] = 444   // 修改舊的底層數組

    fmt.Println(my_slice)   // [11 22 33 444 55]
    fmt.Println(new_slice)  // [11 22 33 44 55 66]

    fmt.Println(len(my_slice),":",cap(my_slice))     // 5:5
    fmt.Println(len(new_slice),":",cap(new_slice))   // 6:10
}
           

從上面的結果上可以發現,底層數組被擴容為10,且是新的底層數組。

實際上,當底層數組需要擴容時,會按照目前底層數組長度的2倍進行擴容,并生成新數組。如果底層數組的長度超過1000時,将按照25%的比率擴容,也就是1000個元素時,将擴充為1250個,不過這個增長比率的算法可能會随着go版本的遞進而改變。

實際上,上面的說法應該改一改:當capacity需要擴容時,會按照目前capacity的2倍對數組進行擴容。或者說,是按照slice的本質

[x/y]0xADDR

的容量y來判斷如何擴容的。之是以要特别強調這兩種不同,是因為很容易搞混。

例如,擴容的對象是底層數組的真子集時:

1
2
3
4
5
6
7
              my_slice := []int{11,22,33,44,55}

// 限定長度和容量,且讓長度和容量相等
new_slice := my_slice[1:3:3]   // [22 33]

// 擴容
app_slice := append(new_slice,4444)
           

上面的

new_slice

的容量為2,并沒有對應到底層數組的最結尾,是以

new_slice

my_slice

的一個真子集。這時對

new_slice

擴容,将生成一個新的底層數組,新的底層數組容量為4,而不是10。如下圖:

因為建立了新的底層數組,是以修改不同的slice,将不會互相影響。為了保證每次都是修改各自的底層數組,通常會切出僅一個長度、僅一個容量的新slice,這樣隻要對它進行任何一次擴容,就會生成一個新的底層數組,進而讓每個slice的底層數組都獨立。

1
2
3
4
              my_slice := []int{11,22,33,44,55}

new_slice := my_slice[2:3:3]
app_slice := append(new_slice,3333)
           

事實上,這還是會出現共享的幾率,因為沒有擴容時,那個唯一的元素仍然是共享的,修改它肯定會影響至少1個slice,隻有切出長度為0,容量為0的slice,才能完全保證獨立性,但這和新建立一個slice沒有差別。

合并slice

slice和數組其實一樣,都是一種值,可以将一個slice和另一個slice進行合并,生成一個新的slice。

合并slice時,隻需将append()的第二個參數後加上

...

即可,即

append(s1,s2...)

表示将s2合并在s1的後面,并傳回新的slice。

1
2
3
4
5
6
              s1 := []int{1,2}
s2 := []int{3,4}

s3 := append(s1,s2...)

fmt.Println(s3)  // [1 2 3 4]
           

注意append()最多允許兩個參數,是以一次性隻能合并兩個slice。但可以取巧,将append()作為另一個append()的參數,進而實作多級合并。例如,下面的合并s1和s2,然後再和s3合并,得到

s1+s2+s3

合并後的結果。

sn := append(append(s1,s2...),s3...)
           

slice周遊疊代

slice是一個集合,是以可以進行疊代。

range關鍵字可以對slice進行疊代,每次傳回一個index和對應的元素值。可以将range的疊代結合for循環對slice進行周遊。

1
2
3
4
5
6
7
8
              package main

func main() {
    s1 := []int{11,22,33,44}
    for index,value := range s1 {
        println("index:",index," , ","value",value)
    }
}
           

輸出結果:

1
2
3
4
              index: 0  ,  value 11
index: 1  ,  value 22
index: 2  ,  value 33
index: 3  ,  value 44
           

傳遞slice給函數

前面說過,雖然slice實際上包含了3個屬性,它的資料結構類似于

[3/5]0xc42003df10

,但仍可以将slice看作一種指針。這個特性直接展現在函數參數傳值上。

Go中函數的參數是按值傳遞的,是以調用函數時會複制一個參數的副本傳遞給函數。如果傳遞給函數的是slice,它将複制該slice副本給函數,這個副本實際上就是

[3/5]0xc42003df10

,是以傳遞給函數的副本仍然指向源slice的底層數組。

換句話說,如果函數内部對slice進行了修改,有可能會直接影響函數外部的底層數組,進而影響其它slice。但并不總是如此,例如函數内部對slice進行擴容,擴容時生成了一個新的底層數組,函數後續的代碼隻對新的底層數組操作,這樣就不會影響原始的底層數組。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
              package main

import "fmt"

func main() {
    s1 := []int{11, 22, 33, 44}
    foo(s1)
    fmt.Println(s1[1])     // 輸出:23
}

func foo(s []int) {
    for index, _ := range s {
        s[index] += 1
    }
}
           

上面将輸出23,因為foo()直接操作原始的底層數組,對slice的每個元素都加1。

slice和記憶體浪費問題

由于slice的底層是數組,很可能數組很大,但slice所取的元素數量卻很小,這就導緻數組占用的絕大多數空間是被浪費的。

特别地,垃圾回收器(GC)不會回收正在被引用的對象,當一個函數直接傳回指向底層數組的slice時,這個底層數組将不會随函數退出而被回收,而是因為slice的引用而永遠保留,除非傳回的slice也消失。

是以,當函數的傳回值是一個指向底層數組的資料結構時(如slice),應當在函數内部将slice拷貝一份儲存到一個使用自己底層數組的新slice中,并傳回這個新的slice。這樣函數一退出,原來那個體積較大的底層數組就會被回收,保留在記憶體中的是小的slice。