1. 程式人生 > >《Go語言核心36講》筆記8:陣列與切片

《Go語言核心36講》筆記8:陣列與切片

回顧

前面幾節都是關於Go語言的基礎知識,包括開發環境配置、常用原始碼檔案語法,以及程式實體、變數及其相關概念和技巧(如型別推斷、變數重宣告、可重名變數、型別斷言、型別轉換、別名型別和潛在型別等),這些都是學習Go語言的基礎,務必要清楚每一個細節,也可以自己寫程式碼實踐一下,加深印象。

前言

正式進入第二階段的學習,主要討論Go語言的陣列(array)和切片(slice)型別,它們都屬於集合型別,它們的值可以用來儲存某一種型別的值(或者元素)。

陣列型別的值的長度是固定的,在宣告陣列的時候,長度必須給定,並且在之後不會改變,可以說陣列的長度是其型別的。

比如:[1]string和[2]string就是兩個不同的陣列型別。

切片型別的值是可變長的。切片的型別字面量中只有元素的型別([]int),而沒有長度。切片的長度可以自動地隨著其中元素數量的增長而增加,但是不會隨著元素數量的減少而減小。可以把切片看作是對陣列的一層簡單的封裝,因為在每個切片的底層資料結構中,一定會包含一個數組。後者被叫做前者的底層陣列,前者被看作是對後者的某個連續片段的引用

Go語言的切片型別屬於“引用型別”,同屬引用型別的還有字典型別、通道型別、函式型別;

陣列型別屬於值型別,同屬值型別的還有基礎資料型別(int,int8,int16等)及結構體型別。

如果傳遞的值是引用型別的,那麼就是“傳引用”(符號&);

如果傳遞的值是值型別的,那麼就是“傳值”(符號=);

從傳遞成本的角度講,引用型別(&)的值要比值型別的值低很多。

在陣列和切片之上都可以應用索引表示式([x]),得到某個元素;應用切片表示式([x:y]),得到新的切片。

len函式可以計算長度;cap函式可以計算容量。

陣列的容量永遠對於其長度,都是不可變的。

切片的容量是可變的,而且其變化有規律可尋。

問題:怎樣正確估算切片的長度和容量

見示例demo15.go:

// demo15.go
package main

import (
	"fmt"
)

func main() {
	s1:=make([]int, 5)// 指明長度
	fmt.Printf("The value of s1 is: %v\n", s1)
	fmt.Printf("The length of s1 is: %d\n", len(s1))
	fmt.Printf("The capacity of s1 is: %d\n", cap(s1))
	fmt.Printf("The value of s1 is: %d\n", s1)
	s2:=make([]int, 5, 8)// 指明長度和容量
	fmt.Printf("The length of s2:%d\n", len(s2))
	fmt.Printf("The capacity of s2 is:%d\n", cap(s2))
	fmt.Printf("The value of s2 is:%d\n", s2)
}

示例中,切片s1和s2的容量分別是5和8.

問題解析

s1 := make([]int, 5)

s1的容量為5,在宣告的時候就已指定。

make函式在初始化切片時,如果不明確指定容量,那麼其長度就和容量一致。如果指明瞭容量,那麼該容量就是實際容量。

s2 := make([]int, 5, 8)

在s2中,也指定了長度和容量,該容量實際上代表了s2底層陣列的長度,這裡是8.

注意:切片的底層陣列長度是不可變的。

想象有一個視窗,通過這個視窗可以看到一個數組,但是不一定能看到該陣列中所有元素,有一部分是被擋住了,有時候只能看到連續的一部分元素。

該例中,這個陣列就是切片s2的底層陣列,而這個視窗就是s2本身。s2的長度就是這個視窗的寬度,決定了你透過s2可以看到其底層陣列中的哪幾個連續的元素。

由於s2的長度是5(定義的),所以你可以看到其底層陣列中的第1到第5個元素,對應的底層陣列的索引範圍是0到4([0,4])。

切片代表的視窗被劃分成一個個的小格子,每個小格子都對應著其底層陣列中的某一個元素。

s2中,視窗最左邊的小格子對應的正好是其底層陣列中的第一個元素,即索引為0的那個元素。因此,s2中的索引從0到4的元素,就是其底層陣列中索引從0到4的那5個元素。

長度

當我們用make函式或切片值字面量(如:[]int{1,2,3})初始化一個切片時,該視窗最左邊的那個小格子總是會對應其底層陣列中的第1個元素。

但是當我們通過“切片表示式”基於某個陣列或者切片生成新切片的時候,情況就複雜一點,如:

s3 := []int{1,2,3,4,5,6,7,8}
s4 := s3[3:6]

切片s3中有8個元素(整數1到8),s3的長度和容量都是8.

s4從s3中通過切片表示式初始化而來,其中[3:6]表達的就是透過新視窗能看到的,用減法可以計算出長度,6減去3為3.而索引範圍為從3到5(不包括6,可以引申為區間表示法,即[3,6))。

s4中的索引從0到2,指向的元素對應的是s3中索引從3(起始索引)到5(結束索引)的3(長度)個元素。

容量

切片的容量代表了它的底層陣列的長度,但這僅限於用make函式或者切片字面量([]int{1,2,3})初始化切片的情況。

一個切片的容量可以被看作是透過這個視窗最多可以看到的底層陣列中元素的個數。

s4是通過在s3上施加切片操作得來的,所以s3的底層陣列等於s4的底層陣列;而且,在底層陣列不變的情況下,切片代表的視窗可以向右擴充套件,直至其底層陣列的末尾,所以s4的容量就是其底層陣列的長度8減去上述切片表示式([3:6])中的起始索引3,等於5。

要注意的是,切片代表的視窗是無法向左擴充套件的,也即無法透過s4看到s3中最左邊的那3個元素。

通過切片表示式:

s4[0:cap(s4)]

可以把切片的視窗向右擴充套件到最大,該表示式會產生一個新的切片[]int{4,5,6,7,8},長度和容量都是5.

知識擴充套件

詳細介紹切片的長度和容量計算。

問題1:怎樣估算切片容量的增長

如果一個已定義的切片無法容納更多的元素,Go就會自動為其擴容。但是,擴容不會改變原來的切片,而是會生成一個容量更大的切片,然後把原切片元素和新元素都複製到新切片中。

對於擴容的計算方法,有兩種:

1、一般情況下,新容量是原容量的2倍;

2、當原長度>=(大於或等於)1024時,新容量基準不再是2倍,而是1.25倍。新容量基準會在此基礎上不斷地被調整,直到不小於原長度與要追加的元素數量之和(新長度),最終,新容量往往會比新長度大一些,也有可能相等。

特殊情況:如果一次追加的元素過多,以至於使得新長度比原容量的2倍還要大,那麼新容量就會以新長度為基準,最終的新容量往往比新容量基準要更大一些。

問題2:切片的底層陣列什麼時候會被替換

注意:一個切片的底層陣列永遠不會被替換

因為在“擴容”的時候,也一定會生成新的底層陣列,同時也生成新的切片,而對原切片及其底層陣列沒有做任何改動。

append函式使用注意:在無需擴容時,返回的是指向原底層陣列的切片;需要擴容時,返回的是指向新底層陣列的新切片。

只要新長度不會超過切片的原容量,那麼使用append函式對其追加元素的時候就不會引起擴容,這隻會使得緊鄰切片視窗(抽象)右邊的元素被新的元素替換掉。示例demo17.go:

// demo17.go
package main

import (
	"fmt"
)

func main() {
	a1:=[7]int{1,2,3,4,5,6,7}
	fmt.Printf("a1: %v (length: %d, cap: %d)\n",
		a1, len(a1), cap(a1))
	s9:=a1[1:4]
	fmt.Printf("s9: %v (length: %d, cap: %d)\n", 
		s9, len(s9), cap(s9))
	for i:=1; i<=5; i++{
		s9 = append(s9, i)
		fmt.Printf("s9(%d): %v (length: %d, cap: %d)\n",
			i, s9, len(s9), cap(s9))
	}
	fmt.Printf("a1: %v (length: %d, cap: %d)\n",
		a1, len(a1), cap(a1))
	fmt.Println()
}

執行結果:

$ go run demo17.go
a1: [1 2 3 4 5 6 7] (length: 7, cap: 7)
s9: [2 3 4] (length: 3, cap: 6)
s9(1): [2 3 4 1] (length: 4, cap: 6)
s9(2): [2 3 4 1 2] (length: 5, cap: 6)
s9(3): [2 3 4 1 2 3] (length: 6, cap: 6)
s9(4): [2 3 4 1 2 3 4] (length: 7, cap: 12)
s9(5): [2 3 4 1 2 3 4 5] (length: 8, cap: 12)
a1: [1 2 3 4 1 2 3] (length: 7, cap: 7)

總結

切片是基於陣列的,長度可變,容量固定,一個切片只會與某一個底層陣列繫結在一起。

切片的容量是介於切片長度和底層陣列之間的某一個值,並且與切片視窗(抽象叫法)最左邊對應的元素在底層陣列中的位置有關係。

計算切片長度和容量的方法:

1、結束索引減去起始索引,就是切片的長度。

如[3:6]的長度為6減去3,等於3,但是索引範圍是3到5(不包括6)。

2、容量為切片底層陣列的長度減去切片的起始索引(因為無法向做擴充套件)。

append(param, int)函式執行後,返回的是新的切片的長度,如果新切片的容量比原切片的容量更大,說明底層陣列也被更新了。

要學會對切片的“擴容”策略進行近似估算。

思考題

關於陣列和切片,其實還有很多細節需要去關注,這裡提出兩個思考題:

1、如果有多個切片指向了同一個底層陣列,應該注意什麼?

2、怎樣沿用“擴容”的思想來對切片進行“縮容”?用程式碼實踐下。

本系列筆記摘錄自極客時間的《Go語言核心36講》,版權歸極客時間所有。