非懂不可的Slice(一)-- 就要學習Go語言

Seekload發表於2018-12-12

前言

切片是一種複合資料型別,與陣列類似,存放相同資料型別的元素,但陣列的大小是固定的,而切片的大小可變,可以按需自動改變大小。切片是基於底層陣列實現的,是對陣列的抽象。切片很小,只有三個欄位的資料結構:指向底層陣列的指標、能訪問的元素個數(即切片長度)和允許增長到的元素個數(即切片容量)。

非懂不可的Slice(一)-- 就要學習Go語言
如上圖所示,一個長度為3、容量為5的整型切片的底層結構。

宣告與初始化

make()建立

使用內建函式建立空切片,形如:

s := make([]type, len, cap)  // len 長度,cap 容量
複製程式碼

也可以只指定len,那麼切片的容量和長度是一樣的。Go語言提供了內建函式lencap分別返回切片的長度和容量。

// 宣告一個長度為3、容量為5的整型切片
s1 := make([]int,3,5)
fmt.Println(len(s1),cap(s1))   // 輸出:3 5

// 宣告一個長度和容量都是5的字串切片
s2 := make([]string,5)
fmt.Println(len(s2),cap(s2))   // 輸出:5 5
複製程式碼

切片建立完成,如果不指定字面量的話,預設值就是陣列的元素的零值。
切片的容量就是切片底層陣列的大小,我們只能訪問切片長度範圍內的元素,如第一節的圖所示,長度為3的整型切片存入3個值後的結構,我們只能訪問到第3個元素,剩餘的2個元素需要切片擴充以後才可以訪問。所以,很明顯的:容量>=長度,我們不能建立長度大於容量的切片。

s1 := make([]int,5,3)
// 報錯:len larger than cap in make([]int)
複製程式碼

使用字面量建立切片

使用字面量建立,就是指定了初始化的值

s := []int{1,2,3,4,5}     // 長度和容量都是5的整型切片
複製程式碼

有沒有發現,這種建立方式與建立陣列類似,只不過不用指定[]的值,這時候切片的長度和容量是相等的,並且會根據指定的字面量推匯出來。
區別:

// 建立大小為10的陣列
s := [10]int{1,2,3,4,5}
// 建立切片
s := []int{1,2,3,4,5}
複製程式碼

我們也可以只初始化某一個索引的值:

s := []int{4:1}
fmt.Println(len(s),cap(s))  // 輸出:5 5
fmt.Println(s)		// 輸出:[0 0 0 0 1]
複製程式碼

指定了第5個元素為1,其他元素初始化為0。

基於已有的陣列或者切片建立切片

使用操作符[start:end],簡寫成[i:j],表示從索引i,到索引j結束,擷取已有陣列或者切片的任意部分,返回一個新的切片,新切片的值包含原切片的i索引的值,但是不包含j索引的值。ij都是可選的,i如果省略,預設是0,j如果省略,預設是原切片或陣列的長度。ij都不能超過這個長度值。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

fmt.Println("s[:]", s[:])
fmt.Println("s[2:]", s[2:])
fmt.Println("s[:4]", s[:4])
fmt.Println("s[2:4]", s[2:4])
複製程式碼

輸出
你可能會有個疑問:擷取獲得的新切片的長度和容量怎麼計算呢?我們當然可以使用內建函式lencap直接獲得,如果明白了怎麼計算的,我們處理問題就可以更得心應手。
對底層陣列大小為k的切片執行[i,j]操作之後獲得的新切片的長度和容量是: 長度:j-i
容量:k-i

就拿上一個例子的s[2:4]來說,原切片底層陣列大小是10,所以新切片的長度是4-2=2,容量是10-2=8。 可以使用內建函式驗證下:

s1 := s[2:4]
fmt.Println(len(s1),cap(s1)) // 輸出:2 8
複製程式碼

上面是使用2個索引的方法建立切片,還可以使用3個索引的方法,第3個用來限定新切片的容量,用法為slice[i:j:k]

s2 := s[2:4:8]
fmt.Println(s2)  // 輸出:[2 3]
複製程式碼

長度和容量如何計算:長度j-i,容量k-i。所以切片s2的長度和容量分別是26注意k不能超過原切片(陣列)的長度,否則報錯panic: runtime error: slice bounds out of range
例如上面的例子中,第三個索引值不能超過10。
我們來看個例子:

s := []int{0, 1, 2, 3, 4, 5}

fmt.Println("before,s:",s)
s1 := s[1:4]
fmt.Println("before,s1:",s1)
s1[1] = 10
fmt.Println("after,s1:",s1)
fmt.Println("after,s:",s)
複製程式碼

輸出

before,s: [0 1 2 3 4 5]
before,s1: [1 2 3]
after,s1: [1 10 3]
after,s: [0 1 10 3 4 5]
複製程式碼

這個例子說明,原切片和新切片是基於同一個底層陣列的,所以當修改的時候,底層陣列的值就會被改變,原切片的值也隨之改變了。對於基於陣列的切片也一樣的。

在這裡插入圖片描述
我們可以看到,執行完切片動作之後,獲得一個新切片,與原切片共享同一段底層陣列,但通過不同的切片會看到底層陣列的不同部分。切片s能夠看到底層陣列全部6個元素,而切片s1只能看到索引1及之後的全部元素,對於s1來說,索引1之前的部分是不存在。

使用切片

切片的使用方法與陣列的使用方法類似,直接通過索引就可以獲取、修改元素的值。

s := []int{1, 2, 3, 4, 5}
fmt.Println(s[1])   // 獲取值   輸出:2
s[1] = 10	  	// 修改值
fmt.Println(s)   //輸出:[1 10 3 4 5]
複製程式碼

只能訪問切片長度範圍內的元素,否則報錯

s := []int{1, 2, 3, 4, 5}
s1 := s[2:3]			
fmt.Println(s1[1]) 
複製程式碼

上面這個例子中,s1的容量為3,長度為1,所以只能訪問s1第一個元素s1[0],訪問s1[1]就會報錯:panic: runtime error: index out of range

與切片的容量相關聯的元素只能用於增長切片,在使用這部分元素前,必須將其合併到切片的長度裡。

相較於陣列,使用切片的好處在於,可以按需增長,類似於動態陣列。Go提供了內建append函式,能夠幫我們處理切片增長的一些列細節,我們只管使用就可以了。
函式原型:

func append(slice []Type, elems ...Type) []Type
複製程式碼

使用append函式,需要一個被操作的切片和一個(多個)追加值,返回一個相同資料型別的新切片。

s := []int{1, 2, 3, 4, 5}
newS := s[2:4]
newS = append(newS, 50)
fmt.Println(s, newS)
fmt.Println(&s[2] == &newS[0])
複製程式碼

輸出

[1 2 3 4 50] [3 4 50]
true
複製程式碼

上面的例子中,擷取獲得一個長度為2,容量為3(可用容量為1)的新切片newS,通過append函式向切片newS追加一個元素50。 追加元素50之前:

追加元素10之前
追加元素50之後:
追加元素10之後
通過輸出結果可以得出,新切片newS與原切片s是共享底層陣列的,當切片可用容量能夠存下追加元素時,不會建立新的切片。
當切片可用容量存不下需要追加的元素時會發生呢?

s := []int{1, 2, 3, 4, 5, 6, 7, 8}
s1 := s[2:4]
fmt.Printf("before -> s=%v\n", s)
fmt.Printf("before -> s1=%v\n", s1)
fmt.Printf("before -> len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Println("&s[2] == &s1[0] is", &s[2] == &s1[0])

s1 = append(s1, 60, 70, 80, 90, 100, 110)
fmt.Printf("after -> s=%v\n", s)
fmt.Printf("after -> s1=%v\n", s1)
fmt.Printf("after -> len=%d, cap=%d\n", len(s1), cap(s1))
fmt.Println("&s[2] == &s1[0] is", &s[2] == &s1[0])
複製程式碼

輸出

before -> s=[1 2 3 4 5 6 7 8]
before -> s1=[3 4]
before -> len=2, cap=6
&s[2] == &s1[0] is true
after -> s=[1 2 3 4 5 6 7 8]
after -> s1=[3 4 60 70 80 90 100 110]
after -> len=8, cap=12
&s[2] == &s1[0] is false
複製程式碼

追加元素60、70、80、90、100和110之前:

在這裡插入圖片描述
追加之後:
非懂不可的Slice(一)-- 就要學習Go語言
從結果可以看出,切片的底層陣列沒有足夠的可用容量, append函式會建立一個新的底層陣列,將原陣列的值複製到新陣列裡,再追加新的值,就不會影響原來的底層陣列。

一般我們在建立新切片的時候,最好要讓新切片的長度和容量一樣,這樣我們在追加操作的時候就會生成新的底層陣列,和原有陣列分離,就不會因為共用底層陣列而引起奇怪問題,因為共用陣列的時候修改內容,會影響多個切片。

append函式會智慧地增加底層陣列的容量,目前的演算法是:當陣列容量<=1024時,會成倍地增加;當超過1024,增長因子變為1.25,也就是說每次會增加25%的容量。
Go提供了...操作符,允許將一個切片追加到另一個切片上:

s := []int{1, 2,3,4,5}
s1 := []int{6,7,8}
s = append(s,s1...)
fmt.Println(s,s1)
複製程式碼

輸出:

[1 2 3 4 5 6 7 8] [6 7 8]
複製程式碼

迭代切片

使用for迴圈迭代切片,配合len函式使用:

s := []int{1, 2, 3, 4, 5}
for i:=0;i<len(s) ;i++  {
	fmt.Printf("Index:%d,Value:%d\n",i,s[i])
}
複製程式碼

使用for range迭代切片:

s := []int{1, 2, 3, 4, 5}
for i,v := range s {
	fmt.Printf("Index:%d,Value:%d\n",i,v)
}

// 使用‘_’可以忽略返回值
s := []int{1, 2, 3, 4, 5}
for _,v := range s {
	fmt.Printf("Value:%d\n",v)
}
複製程式碼

需要注意的是,range返回的是切片元素的複製,而不是元素的引用。如果使用該值變數的地址作為指向每個元素的指標,就會造成錯誤。

s := []int{1, 2, 3, 4, 5}
for i,v := range s {
	fmt.Printf("v:%d,v_addr:%p,elem_addr:%p\n",v,&v,&s[i])
}
複製程式碼

輸出

v:1,v_addr:0xc000018058,elem_addr:0xc000016120
v:2,v_addr:0xc000018058,elem_addr:0xc000016128
v:3,v_addr:0xc000018058,elem_addr:0xc000016130
v:4,v_addr:0xc000018058,elem_addr:0xc000016138
v:5,v_addr:0xc000018058,elem_addr:0xc000016140
複製程式碼

可以看到,v的地址總是相同的,因為迭代返回的變數在迭代過程中根據切片依次賦值的新變數。 好了,今天先講到這裡,下一節,我們再來討論關於Slice更多的用法!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。

公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公眾號二維碼

相關文章