跟著老貓來搞GO-容器(1)

程式設計師老貓發表於2021-11-15

前期回顧

前面的一章主要和大家分享了GO語言的函式的定義,以及GO語言中的指標的簡單用法,那麼本章,老貓就和大家一起來學習一下GO語言中的容器。

陣列

陣列的定義

說到容器,大家有程式設計經驗的肯定第一個想到的就是陣列了,當然也有程式設計經驗的小夥伴會覺得陣列並不是容器。但是無論如何,說到陣列其實它就是儲存和組織資料的一種方式而已,大家就不要太過糾結叫法了。

我們們直接上陣列定義的例子,具體如下:

var arr1 [5]int //定義一個長度為5的預設型別
arr2:=[3]int{1,2,3} //定義一個陣列,並且指定長度為3
arr3:=[...]int{1,2,3,4,5,6} //定義一個陣列,具體的長度交給編譯器來計算
var grid [4][5] bool //定義一個四行五列的二維陣列
fmt.Println(arr1,arr2,arr3,grid)

上面的例子輸出的結果如下

[0 0 0 0 0] [1 2 3] [1 2 3 4 5 6] [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

大家可以總結一下,其實陣列有這麼幾個特點

  • 在寫法上,其實也是和其他程式語言是相反的,其定義的陣列的長度寫在變數型別的前面
  • 陣列中所儲存的內容必然是同一型別的

陣列的遍歷

那麼我們如何遍歷獲取陣列中的資料呢?其實看過老貓之前文章的小夥伴應該曉得可以用for迴圈來遍歷獲取,其中一種大家比較容易想到的方式如下(我們以遍歷上面的arr3為例)

for i:=0;i<len(arr3);i++ {
    fmt.Println(arr3[i])
}

這種方式呢,我們當然是可以獲取的。接下來老貓其實還想和大家分享另外一種方式,採用range關鍵字的方式

//i表示的是資料在陣列中的位置下標,v表示實際的值
for i,v :=range arr3 {
	fmt.Println(i,v)
}
//那麼如果我們只想要value值呢,回顧一下老貓之前所說的就可以曉得,我們可以用_的方式進行對i省略
for _,v :=range arr3 {
	fmt.Println(v)
}
//如果我們們只要位置下標,那麼我們如下去寫即可
for i:=range arr3 {
	fmt.Println(i)
}

大家覺得上述兩種方式哪種方式會比較優雅?顯而易見是後者了,意義明確而且美觀。

go語言中陣列是值傳遞的

另外和大家同步一點是陣列作為引數也是值傳遞。還是沿用之前的我們重新定義一個新的函式如下:

func printArray(arr [5]int){
	for i,v:=range arr {
		println(i,v)
	}
}

那麼我們在main函式中進行相關呼叫(為了演示編譯錯誤,老貓這裡用圖片)

編譯錯誤演示

大家根據上面的圖可以很清晰的看到呼叫printArray(arr2)的時候報了編譯錯誤,其實這就是說明,在go語言中,即使同一個型別的陣列,如果不同長度,那麼編譯器還是認為他們是不同型別的。

那麼我們這個時候再對傳入的陣列進行值的變更呢,具體如下程式碼

func main() {
	arr3:=[...]int{1,2,3,4,5} //定義一個陣列,並且長度可變
	printArray(arr3)

	for i,v:=range arr3 {
		println(i,v)
	}
}

func printArray(arr [5]int){
	arr[0] = 300
	for i,v:=range arr {
		println(i,v)
	}
}

大家可以看到,老貓在這裡操作了兩次列印,第一次列印是直接在函式中列印,此時已經更改了第一個值,其函式內部列印的結果為

0 300
1 2
2 3
3 4
4 5

顯然內部的值是變更了,然而我們再看一下外面的函式的列印的值,如下

0 1
1 2
2 3
3 4
4 5

其實並沒有發生變更,這其實說明了什麼呢,這其實說明了在呼叫printArray的時候其實是直接將陣列拷貝一份傳入函式的,外面的陣列並未被更新,這也直接說明了GO語言是值傳遞的引數傳遞方式。

大家在使用這個陣列的時候一定要注意好了,說不準就被坑了。大家可能會覺得這個陣列真難用,其實可以告訴大家一個好訊息,在GO語言中,一般其實不會直接去使用陣列的,我們們用的比較多的還是“切片”

切片

說到切片的話,我們們其實最好是基於上面陣列的基礎上去理解切片。我們們先來看一個例子

func main() {
	arr := [...]int{1,2,3,4,5,6,7}
	fmt.Println("arr[2:6]",arr[2:6])
	fmt.Println("arr[:6]",arr[:6])
	fmt.Println("arr[2:]",arr[2:])
	fmt.Println("arr[:]",arr[:])
}

其實像類似於'[]'這種定義我們就稱呼其為切片,英文成為slice,它表示擁有相同型別元素的可變長度的序列。我們來看一下結果:

arr[2:6] [3 4 5 6]
arr[:6] [1 2 3 4 5 6]
arr[2:] [3 4 5 6 7]
arr[:] [1 2 3 4 5 6 7]

其實這麼說會比較好理解,slice我們們可以將其看作為檢視,就拿arr[2:6]來說,我們其實在原來陣列的基礎上抽取了從第二個位置到第六個位置的元素作為值重新展現出來,當然我們的取值為左閉右開區間的。

slice其實是檢視概念

上面我們說了slice相當於是陣列的檢視,那麼接下來的例子,我們們來證實上述的說法,詳細看下面的例子

func main() {
	arr := [...]int{1,2,3,4,5,6,7}
	fmt.Println("arr[2:6]",arr[2:6])
	updateSlice(arr[2:6])
	fmt.Println("arr[2:6]",arr[2:6])
	fmt.Println(arr)
}

func updateSlice(arr []int){
	arr[0] = 100
}

老貓寫了個函式,主要是更新slice第一個位置的值,大家可以先思考一下執行前後所得到的結果是什麼,然後再看下面的答案。

其實最終執行的結果為:

arr[2:6] [3 4 5 6]
arr[2:6] [100 4 5 6]
[1 2 100 4 5 6 7]

那麼為什麼是這樣的?其實arr[2:6]很容易理解是上面的3456,第二個也比較容易理解,當我們slice的第一個值被更新成了100,所以程式設計了第二種,那麼原始的資料為什麼也會變成100呢?這裡面其實是需要好好品一下,因為我們之前說slice是對原陣列的檢視,當我們第二種看到slice其實已經發生了更新變成了100,那麼底層的資料肯定也發生了變更,變成了100了。(這裡要注意的是,並沒有誰說檢視的操作不會反作用於原陣列)。這裡還是比較重要的,希望大家細品一下。

reslice以及擴充套件

說到reslice,說白了就是對原先的slice再做一次slice取值,那麼我們看下面的例子。

func main() {
	arr := [...]int{1,2,3,4,5,6,7}
	s1 := arr[:]
	s2 := s1[2:4]
	fmt.Println(s2)
}

以上例子可見s1是對陣列的全量切片,然後我們對s1又進行了一次切片處理,很容易地可以推算出來我們第二次所得到的結果為[3,4],像這種行為我們就稱為reslice,這個還是比較好理解的。

接下來我們們在這個基礎上加深一下難度,我們在S2的基礎上再次進行resilce,具體如下:

func main() {
	arr := [...]int{1,2,3,4,5,6,7}
	s1 := arr[:]
	s2 := s1[2:4]
	s3 := s2[1:3]
	fmt.Println(s3)
}

我們都知道s2所得到的值為[3,4],當我們在次對其進行reslice的時候,由於取的是[1:3],那麼此時我們發現是從第一個位置到第三個位置,第一個位置還是比較好推算出來的,基於[3,4]的話,那麼其第一個位置應該是4,那麼後面呢?結果又是什麼呢?這裡將結果直接告訴大家吧,其實老貓執行之後所得到的結果是

[4 5]

那麼為什麼會有這樣的一個結果?5又是從哪裡來的呢?

我們們來看一下老貓下面整理的一幅示意圖。
示意圖

  1. arr的一個陣列,並且其長度為7,並且裡面儲存了七個數。
  2. 接下來s1對其去完全切片,所以我們得到的也是一個完整的7個數。
  3. 需要注意的是,這時候我們用的是下標表示,當s2對s1在此切片的時候,我們們發現其本質是對陣列的第二個元素開始進行取值,由於是檢視的概念,其實s2還會檢視arr虛幻出另外兩個位置,也就是我們們表示的灰色的3以及4下標。
  4. 同樣的我們將s3表示出來,由此我們s3是在s2的基礎上再次切片,理論上有三個下標值,分別是0、1、2下標取值,但是我們發現s2的3號位置指示虛幻出來的位置,並未真正存在值與之對應,因此,我們們取交集之後與陣列arr對應只能取出兩個,也就是最終的[4,5]。

此處還是比較難理解,希望大家好好理解一下,然後寫程式碼自己推演一下,其實這個知識點就是slice的擴充套件,我們再來看一下下面的slice的底層實現。
底層結構

其實slice一般包含三個概念,slice的底層其實是空陣列結構,ptr為指向陣列第一個位置的指標,Len表示具體的slice的可用長度,而cap表示有能力擴充套件的長度。

其實關於len以及cap我們都有函式直接可以呼叫獲取,我們看一下上面的例子,然後列印一下其長度以及擴充套件cap大家就清楚了。具體列印的程式碼如下。

func main() {
	arr := [...]int{1,2,3,4,5,6,7}
	s1 := arr[:]
	s2 := s1[2:4]
	s3 := s2[1:3]
	fmt.Printf("arr=%v\n",arr)
	fmt.Printf("s1=%v,len(s1)=%d,cap(s1)=%d\n",s1,len(s1),cap(s1))
	fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
	fmt.Printf("s3=%v,len(s3)=%d,cap(s3)=%d\n",s3,len(s3),cap(s3))
}

上述程式碼輸出的結果為

arr=[1 2 3 4 5 6 7]
s1=[1 2 3 4 5 6 7],len(s1)=7,cap(s1)=7
s2=[3 4],len(s2)=2,cap(s2)=5
s3=[4 5],len(s3)=2,cap(s3)=4

當我們的取值超過cap的時候就會報錯,例如現在s2為s2:=[2:4],現在我們發現其cap為5,如果我們超過5,那麼此時s2可以寫成s2:=[2:8],那麼此時就會報以下異常

panic: runtime error: slice bounds out of range [:8] with capacity 7

goroutine 1 [running]:
main.main()
	E:/project/godemo/part6-slice.go:8 +0x7f

再者如果我們這麼取值

fmt.Printf("s3=%v",s3[4])

此時s3已經超過了len長度,那麼也會報錯,報錯如下

panic: runtime error: index out of range [4] with length 2

goroutine 1 [running]:
main.main()
	E:/project/godemo/part6-slice.go:14 +0x49f

綜上例子,我們其實可以得到這麼幾個結論。

  1. slice可以向後擴充套件,不可以向前擴充套件。
  2. s[i]不可以超越len(s),向後擴充套件不可以超越底層陣列cap(s)

以上對slice的擴充套件其實還是比較讓人頭疼的,比較難理解,不過真正弄清裡面的演算法倒是也還好,希望大家也能理解上述的闡釋,老貓已經盡最大努力了,如果還有不太清楚的,也歡迎大傢俬聊老貓。

切片的操作

向slice新增元素,如何新增呢?看一下老貓的程式碼,如下:

func main() {
	arr :=[...]int{0,1,2,3,4,5,6,7}
	s1 :=arr[2:6]
	s2 :=s1[3:5]
	s3 := append(s2,10) //[5,6,10]
	s4 := append(s3,11) //[5,6,10,11]
	s5 := append(s4,12)
	fmt.Printf("arr=%v\n",arr)
	fmt.Printf("s2=%v,len(s2)=%d,cap(s2)=%d\n",s2,len(s2),cap(s2))
	fmt.Printf("s2=%v\n",s2)
	fmt.Printf("s3=%v\n",s3)
	fmt.Printf("s4=%v\n",s4)
	fmt.Printf("s5=%v\n",s5)
}

如上述所示,我們往切片中新增操作的時候採用的是append函式,大家可以先不看老貓下面的實際結果自己推算一下最終的輸出結果是什麼。結合之前老貓所述的切片操作。結果如下:

arr=[0 1 2 3 4 5 6 10]
s2=[5 6],len(s2)=2,cap(s2)=3
s2=[5 6]
s3=[5 6 10]
s4=[5 6 10 11]
s5=[5 6 10 11 12]

上述我們會發現append操作的話會有這樣的一個結論

  • 新增元素的時候如果超過cap,系統會重新分配更大的底層陣列
  • 由於值傳遞的關係,必須接收append的返回值

slice的建立、拷貝

之前老貓和大家分享的slice看起來都是基於arr的,其實slice的底層也確實是基於arry的,那麼我們是不是每次在建立slice的時候都需要去新建一個陣列呢?其實不是的,我們slice的建立方式有很多種,我們來看一下下面的建立方式

func main() {
	var s []int //1、空slice的建立方式,其實底層是基於Nil值的陣列建立而來
	for i := 0;i<100;i++ {
		s = append(s,2*i+1)
	}
	fmt.Println(s)
    s1 :=[]int {2,4,5,6} //2、建立一個帶有初始化值得slice
    s2 :=make([]int ,16) //3、採用make內建函式建立一個長度為16的切片
    s3 :=make([]int,10,32) //4、採用make內建函式建立一個長度為10的切片,但是cap為32
    //slice的拷貝也是相當簡單的也是直接用內建函式即可,如下
    copy(s2,s1) //這裡主要表示的是將s1拷貝給s2,這裡需要注意的是不要搞反了
}

slice元素的刪除操作

為什麼要把刪除操作單獨拎出來分享,主要是因為上述這些操作都有比較便捷的內建函式來使用,但是刪除操作就沒有了。我們們只能通過切片的特性來求值。如下例子

func main() {
	s1 :=[] int{2,3,4,5,6}
	s2 :=append(s1[:2],s1[3:]...)
	fmt.Println(s2)
}

上述有一個2到6的切片,如果我們要移除其中的4元素,那麼我們就得用這種切片組合的方式去移除裡面的元素,相信大家可以看懂,至於“s1[3:]...”這種形式,其實是go語言的一種寫法,表示取從3號位置剩下的所有的元素。

最終我們得到的結果就得到了

[2 3 5 6]

以上就是對slice的所有的知識分享了,花了老貓不少時間整理出來的,老貓也儘量把自己的一些理解說清楚,slice在語言中還是比較重要的。

寫在最後

回顧一下上面的GO語言容器,其實重點和大家分享是slice(切片)的相關定義,操作以及底層的一些原理。弄清楚的話還是比較容易上手的。當然go語言的容器可不止這些,由於篇幅的限制,老貓就不分享其他的容器了,相信在寫下去就沒有耐心看了。後面的容器主要會和大家分享map以及字元和字串的處理。

我是老貓,更多內容,歡迎大家搜尋關注老貓的公眾號“程式設計師老貓”。

相關文章