1. 程式人生 > >手把手golang教程【二】——陣列與切片

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

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

![](https://user-gold-cdn.xitu.io/2020/5/17/172200b4ee45ad1e?w=258&h=258&f=png&