跟着老貓來搞GO-內建容器slice


前期回顧

前面的一章主要和大家分享了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以及字符和字符串的處理。

我是老貓,更多內容,歡迎大家搜索關注老貓的公眾號“程序員老貓”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM