go語言之陣列與切片

胡洋®發表於2019-01-25

陣列和切片的相同點及不同點

共同點:

都屬於集合類的型別,它們的值用來儲存某一型別的值。

不同點:

  • 陣列型別的長度是固定的,它必須在宣告它的時候就必須給定,並且之後不會再改變。可以說陣列的長度是陣列型別的一部分,例如[1]string和[2]string就是兩個不同的陣列型別。

  • 切片型別的長度是可變長的,它的切片型別字面量中只有元素的型別而沒有元素的長度。切片的長度可以自動隨著其中元素數量的增長而增長,但不會隨著元素數量的減少而減少。

  • 陣列和切片都有長度和容量的概念,分別可以通過內建函式len()cap()獲取,其中,陣列的容量永遠 永遠等於長度,都是不可變的。而切片的容量雖然相對複雜些但也是有規律可尋,後文後講解如何估算一個切片的長度和容量。

image

本質上來說,我們可以把切片看做是對陣列的一層簡單的封裝,每個切片的底層資料結構都是陣列,它可以看作是對陣列某個連續片段的引用,這裡需要注意的幾點是:

  • Go語言中的切片型別屬於引用型別,它是對陣列某個連續片段的引用。同屬引用型別的還有字典型別、通道型別和函式型別。而陣列型別則屬於值型別,同屬於值型別的有基礎資料型別和結構體型別
  • 在Go語言中,判斷函式所謂的傳值還是傳引用問題只需要看被傳遞的值的型別即可,若傳遞的值是值型別的,那麼就是傳值,反之若傳遞的值是引用型別的,那麼就是引用傳遞。從傳遞成本的角度而言,引用型別的值要比值型別低得多
  • 在陣列和切片之上可以使用索引表示式訪問某個具體的元素,也可以使用切片表示式得到一個新的切片

如何初始化一個切片

我們可以通過切片字面量表示式[]int{1,2,3}和內建make函式make([]int,5,6)初始化一個切片,也可以通過切片表示式基於某個陣列或切片生成新切片,接下來分別描述下這幾種場景:

通過切片表示式初始化切片

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	fmt.Printf("slice length: %d\n", len(s))
	fmt.Printf("slice cap: %d\n", cap(s))
}

// 輸出
// slice length: 6
// slice cap: 6

複製程式碼

通過這種方式初始化的切片其長度和容量都等於初始化時傳入的元素數量。

通過make函式初始化切片

func main() {
	s1 := make([]int, 5)
	s2 := make([]int, 5, 6)
	fmt.Printf("s1 length: %d\n", len(s1))
	fmt.Printf("s1 cap: %d\n", cap(s1))
	fmt.Printf("s2 length: %d\n", len(s2))
	fmt.Printf("s2 cap: %d\n", cap(s2))
}

// 輸出
// s1 length: 5
// s1 cap: 5
// s1 length: 5
// s1 cap: 6
複製程式碼

內建函式make接收三個引數,第一個引數為切片的型別字面量,第二個變數為切片的長度,第三個變數為切片的容量。當不指明切片容量的時,切片的容量就會和長度一致。

這裡可以把切片看成一個視窗,通過這個視窗可以看到底層的陣列,視窗被劃分成一個一個的小格子,每個格子代表一個陣列元素,但因為視窗大小有限因此不能看到陣列中所有的元素,大部分時候只能看到陣列連續的一部分元素。

當我們通過make函式或切片值字面量初始化的切片,它的第一個元素總是會對應其底層陣列的第一個元素,在這種情況下,切片的容量就等於其底層陣列的長度。拿s2為例,視窗最左邊的格子對應的正好是其底層陣列索引為0的第一個元素,因此,s2中索引從0到4的元素為其底層陣列中索引從0到4代表的那5個元素。

基於某個陣列或切片生成新切片

func main() {
	s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
	s4 := s3[3:6]
	fmt.Printf("The length of s4:%d\n", len(s4))
	fmt.Printf("The capacity of s4:%d\n", cap(s4))
	fmt.Printf("The value of s4:%d\n", s4)
}

// 輸出
// The length of s4:3
// The capacity of s4:5
// The value of s4:[4 5 6]
複製程式碼

在這裡需要明白s3[3:6]切片表示式中方括號兩個整數其實就是數學中的區間表示法,其中3為起始索引,6為終止索引,代表s4從s3中索引為3的元素開始到索引為5的元素結束(不包含6),s4的長度就是6-3=3。因此我們可以說s4中的索引從0到2指向的元素對應的是s3中索引從3到5的那3個元素。

再來看看容量,一個切片的容量可以看做為通過這個視窗最多可以看到的底層陣列重元素的個數。由於s4是在s3的基礎上通過切片操作得來的,所以s4的底層陣列就是s3的底層陣列,因此s4可以向右擴充套件直至陣列末尾,它的容量就是其底層陣列的長度8減去s3的起始索引3,即5。

注意這裡切片是無法向左擴充套件的,因此是永遠無法透過s4的視窗看到s3的前三個元素的。

若要將s4的視窗向右擴充套件到最大,可以通過切片表示式s4[0,cap(s4)]做到,它的結果值為[]int{4,5,6,7,8}

切片容量的增長規律

func main() {
	s5 := []int{1, 2, 3, 4, 5, 6}
	s6 := s5[0:2]
	s6 = append(s6, 66)
	fmt.Printf("The value of s6:%d\n", s6)
	fmt.Printf("The value of s5:%d\n", s5)
}

// 輸出
// The value of s6:[1 2 66]
// The value of s5:[1 2 66 4 5 6]
複製程式碼

可以通過append函式對切片進行擴充套件(這裡append函式返回的是一個新切片,因此需要用切片型別的變數去接收它的返回值),只要新長度不會超過切片的原容量時,那麼使用append函式對其追加元素時就不會引起擴容,只會使得緊鄰切片視窗右邊的(底層陣列中的)元素被新的元素替換掉,生成一個指向原先底層陣列的新切片。

當新長度超過切片的容量時,append函式返回的是指向新底層陣列的新切片,它會生成一個新的底層陣列,然後將原有的元素和新的元素拷貝到新陣列中,然後再生成一個新切片指向這個新的底層陣列,在一般情況下,可以簡單地認為新切片的容量時原切片容量的兩倍。

相關文章