手把手golang教程——數組與切片

本文始發於個人公衆號:TechFlow,原創不易,求個關注


今天是golang專題的第五篇,這一篇我們將會了解golang中的數組和切片的使用。


數組與切片


golang當中數組和C++中的定義類似,除了變量類型寫在後面。

比如我們要聲明一個長度爲10的int型的數組,會寫成這樣:

var a [10]int

數組的長度定義了之後不能改變,這點和C++以及Java是一樣的。但是在我們日常使用的過程當中,除非我們非常確定數組長度不會發生變化,否則我們一般不會使用數組,而是使用切片(slice)。

切片有些像是數組的引用,它的大小可以是動態的,因此更加靈活。所以在我們日常的使用當中,比數組應用更廣。

切片的聲明源於數組,和Python中的list切片類似,我們通過指定左右區間的範圍來聲明一個切片。這裏的範圍和Python一樣,左閉右開。我們來看個例子:

var a [10]int
var s []int = a[0:4]

這是標準的聲明寫法,我們也可以不用var來聲明,而是直接利用數組給切片賦值,比如上面的語句可以寫成這樣:

s := a[:4]

在Python當中,當我們使用切片的時候,解釋器會爲我們將切片對應的數據複製一份。所以切片之後和之前的結果是不同的,但是golang當中則不同。切片和數據對應的是同一份數據,切片只是數組的一個引用,如果原數組的數據發生變化,那麼會連帶着切片中的數據一起變化。

還是剛纔那個例子:

var a [10]int
var s []int = a[0:4]
fmt.Println(s)

這樣我們輸出得到的結果是[0 0 0 0],因爲數組初始化默認值爲0。而假如我們修改一個a中的元素,我們再來打印s,得到的結果就不同了:

var a [10]int
var s []int = a[0:4]
a[0] = 4
fmt.Println(s)

這樣得到的結果就是[4 0 0 0],雖然我們並沒有修改s當中的數據,由於s本質是a的引用,所以a中發生變化會連帶着s一起變化。


進階用法


前面說了,因爲切片比數組更加方便,所以我們日常使用當中都傾向於使用切片,而不是數組。但是根據目前的語法,切片都是從數組當中產生的,這豈不是意味着,我們如果想要使用切片,必須先要創建出一個對應的數組來嗎

golang的設計者考慮到了這個問題,爲了方便我們的使用,golang設計了直接定義切片的方法。

這是一個數組的聲明,我們固定了數組的長度,並且用指定的元素對它進行了初始化。

var a = [3]int{0, 1, 2}

如果我們去掉長度的聲明,那麼它就成了一個切片的聲明:

var a = []int{0, 1, 2}

這樣是同樣可以運行的,在golang的內部下面的語句同樣創建了數組,我們獲取的a是這個數組的一個切片。但是這個數組對我們是不可見的,golang編譯器替我們省略了這個邏輯。


長度和容量


理解了切片和數組之間的關係之後,我們就可以來看它的長度容量這兩個概念了。

這個單詞的英文分別是length和capability,長度指的是切片本身包含的元素的個數,而容量則是切片對應的數組從開始到末尾包含的元素個數。我們可以用len操作來獲取切片的長度,用cap操作來獲取它的容量。

我們來看一個例子,首先我們創建一個切片,然後寫一個函數來打印出一個切片的長度和容量:

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	printSlice(s)
	
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

當我們運行之後得到的結果是這樣的:

這個和我的預期應該是一致的,我們創建出了6個元素的切片,自然它的容量和長度應該都是6,但接下來的操作可能就會有點出入了。

我們對這個切片再進行切片,繼續輸出切片之後的容量和長度:

s = s[:2]
printSlice(s)

運行之後會得到下面這個結果:

我們發現它的長度變成了2,但是容量還是6,這個也不是特別難理解。因爲雖然當前的切片長度變小了,但是它對應的數組並沒有任何變化,所以它的容量應該還是6。

我們繼續,我們繼續切片:

s := []int{1, 2, 3, 4, 5, 6}
s = s[:2]
s = s[:4]
printSlice(s)

得到這樣的結果:

事情開始有點不一樣了,比較令人關注的點有兩個。一個是s在之前切片結束之後的結果長度是2,但是我們居然可以對它切片到下標4的位置。這說明我們在執行切片的時候,執行的對象並不是切片本身,而是切片背後對應的數組。這一點非常重要,如果不能理解這點,那麼切片的很多操作看起來都會覺得匪夷所思難以理解。

第二個點是切片的容量依然沒有發生變化,這樣不會發生變化,那麼我們再換一種切片的方法試試,看看會不會有什麼不同。

s = s[2:]
printSlice(s)

這一次得到的結果就不同了,它是這樣的:

這一次發生變化了,切片的容量變成了4,也就是說變小了,這是爲什麼呢?

原因很簡單,因爲數組的頭指針的位置移動了。數組原本的長度是6,往右移動了兩位,剩下的長度自然就是4了。但是剩下的問題是,爲什麼數組的頭指針會移動呢?

因爲數組的頭指針和切片的位置是掛鉤的,我們前面的切片操作雖然會改變切片中的元素和它的長度,但是都沒有改變切片指針的位置。而這一次我們進行的切片是[2:],當我們執行這個操作的時候,本質上是指針的位置向右移動到了2。

這也是爲什麼切片的容量定義是它對應的數組從開始到末尾元素的個數,而不是對應的數組元素的個數。因爲指針向右移動會改變容量的大小,但是數組本身的長度是沒有變化的

我們來看個例子就明白了:

var a = [6]int{1, 2, 3, 4, 5, 6}
	s := a[:]
	//printSlice(s)
	s = s[:2]
	printSlice(s)
	s = s[2:]
	printSlice(s)
	//s[0] = 4
	fmt.Println(a)

我們這一次使用顯性的切片,我們對s進行一系列切片之後,它的容量變成了4,但是a當中的元素個數還是6,並沒有變化。所以不能簡單將容量理解成數組的長度,而是切片位置到數組末尾的長度。因爲切片操作會改變切片指針的位置,從而改變容量,但是數組的大小是沒有變化的。


make操作


一般在我們使用切片的時候,我們都是把它當做動態數組用的,也就是Python中的list。所以我們一方面不希望關心切片背後數組,另一方面希望能夠有一個區分度較大的構造方法,和創建數組做一個鮮明的區分。

所以基於以上考慮,golang當中爲我們提供了一個make方法,可以用來創建切片。由於make還可以用來創建其他的類型,比如map,所以我們在使用make的時候,需要傳入我們想要創建的變量類型。這裏我們想要創建的是切片,所以我們要傳入切片的類型,也就是[]int,或者是[]float等等。之後,我們需要傳入切片的長度和容量。

比如:

s := make([]int, 0, 5)

我們就得到了一個長度爲0,容量是5的切片。我們也可以只傳入一個參數,如果只傳入一個參數的話,表示切片的長度和容量相等。

像是這樣:

s := make([]int, 5)

我們如果打印這個s的話,會得到[0 0 0 0 0],也就是說golang會爲我們給切片填充零值。


append方法


前面說了和數組比起來切片的使用更加靈活,意味着切片的長度是可變的,我們可以通過使用append方法向切片當中追加元素。

golang中的append方法和Python已經其他語言不同,golang中的append方法需要傳入兩個參數,一個是切片本身,另一個是需要添加的元素,最後會返回一個切片。

所以我們應該寫成這樣:

s := make([]int, 4)
s = append(s, 4)

這麼做的目的也很簡單,因爲切片的長度是動態的,也就意味着切片對應的數組的長度也是可變的,至少是可能增大的。如果當前的數組容量不足以存儲切片的時候,golang會分配一個更大的數組,這時候會返回一個指向新數組的切片。也就是說由於切片底層實現機制的關係,導致了append方法不能做成inplace的,所以必須要進行返回。我猜,這也是由於性能的考慮。


二維切片


最後我們來看看二維切片在golang當中應該怎麼實現,只能要能理解二維,拓展到多維也是一樣。

golang創造二維切片的方式和C++創建二維的vector有些類似,我們一開始先直接定義一個二維的切片,然後用循環往裏面填充。我們定義二維切片的方法和一維的切片類似,只是多了一個方括號而已,之後我們用循環往其中填充若干個一維切片:

mat := make([][]int, 10)
for i := 0; i < 10; i++ {
  mat[i] = make([]int, 10)
}

結尾


到這裏,golang中關於數組和切片的常見的用法就介紹完了。不僅如此,關於切片底層的實現原理,我們也有了一點淺薄的理解。剛開始接觸切片這個概念的時候可能會覺得有點怪,總覺得好像和我們之前學習的語言對不上號,關於容量的概念也不太容易理解,這個是非常正常的,本質上來說,這一切看起來不太正常或者是不太舒服的地方,背後都有創作者的思考,以及爲了性能的權衡。所以,如果你覺得想不通的話,可以多往這個方面思考,也許會有不一樣的收穫。

今天的文章就到這裏,原創不易,掃碼關注我,獲取更多精彩文章。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章