Golang 入門 : 數組


數組是指一系列同一類型數據的集合。數組中包含的每個數據被稱為數組元素(element),這種類型可以是任意的原始類型,比如 int、string 等,也可以是用戶自定義的類型。一個數組包含的元素個數被稱為數組的長度。在 Golang 中數組是一個長度固定的數據類型,數組的長度是類型的一部分,也就是說 [5]int 和 [10]int 是兩個不同的類型。Golang 中數組的另一個特點是占用內存的連續性,也就是說數組中的元素是被分配到連續的內存地址中的,因而索引數組元素的速度非常快。

本文將介紹 Golang 數組的基本概念和用法,演示環境為 ubuntu 18.04 & go1.10.1。

Golang 數組的特點

我們可以把 Golang 數組的特征歸納為以下三點:

  • 固定長度:這意味着數組不可增長、不可縮減。想要擴展數組,只能創建新數組,將原數組的元素復制到新數組。
  • 內存連續:這意味可以在緩存中保留的時間更長,搜索速度更快,是一種非常高效的數據結構,同時還意味着可以通過數值的方式(arr[index])索引數組中的元素。
  • 固定類型:固定類型意味着限制了每個數組元素可以存放什么樣的數據,以及每個元素可以存放多少字節的數據。

數組是個固定長度的數據類型,其長度和存儲元素的數據類型都在聲明數組時確定,並且不能更改。如果需要存儲更多的元素,必須先創建一個更長的數組,然后把原來數組里的數據復制到新數組中。
數組占用的內存是連續分配的,比如我們創建一個包含 5 個整數元素的數組:

arr1 := [5]int{10,20,30,40,50}

數組在內存中的結構類似下圖:

由於內存連續,CPU 能把正在使用的數據緩存更久的時間。而且在內存連續的情況下非常容易計算索引,也就是說可以快速迭代數組里的所有元素。原因是數組的類型信息可以提供每次訪問一個元素時需要在內存中移動的距離,既然數組的每個元素的類型都相同,又是連續分配,因此就可以以固定的速度索引數組中的任意元素,並且速度非常快!

數組的聲明與初始化

聲明數組
聲明數組時需要指定數組的長度和數組中元素的類型,比如聲明一個包含 5 個元素,類型為 int 的數組:

var arr1 [5]int

這里強調一點,數組的類型是包含數組長度的,因此 [5]int 和 [10]int 是兩個不同類型的數組。
在 Go 語言中聲明變量時,總會使用對應類型的零值來初始化變量,數組也不例外。當聲明數組變量時,數組內的每個元素被初始化為對應類型的零值。比如變量 arr1,它的 5 個元素都被初始化成了 int 類型的零值 0。使用 fmt.Println(arr1) 可以看到數組中元素的值:

package main
import "fmt"
func main(){
    var arr1 [5]int
    fmt.Println(arr1)     // 輸出為:[0 0 0 0 0]
}

你可以把此時數組在內存中的狀態想象為下圖所示的樣子:

使用字面量初始化數組
我們可以通過字面量在聲明數組的同時快速的初始化數組:

arr2 := [5]int{10,20,30,40,50}

對於這種情況,還可以使用 … 代替數組的長度,讓編譯器根據實際的元素個數自行推斷數組的長度:

arr3 := […]int{10,20,30,40,50}

如果設置了數組的長度,還可以通過指定下標的方式初始化部分元素:

//  用具體值初始化索引為 1 和 3 的元素
arr4 := [5]int{1:20,3:40}

數組的內容如下圖所示:

訪問與修改數組元素

和其它類 C 語言一樣,Go 語言數組通過數組下標(索引位置)來讀取或者修改數組元素。下標(索引)從 0 開始,第一個元素的索引為 0,第二個索引為 1,依次類推。元素的數目(數組長度)必須是固定的並且在聲明數組時就指定(編譯器需要知道數組的長度以便分配內存),數組長度最大為 2G。

訪問數組元素
對於數組 arr 來說,第一個元素就是 arr[0],第二個元素是 arr[1],最后一個元素則是 arr[len(arr)-1]。下面的代碼定義一個整型數組,然后通過 for 循環打印數組中的每個元素:

package main
import "fmt"
func main(){
    arr := [5]int{10,20,30,40,50}
    for i := 0; i < len(arr); i++ {
        fmt.Printf("At index %d is %d\n", i, arr[i])
    }
}

運行上面的代碼,輸出如下:

At index 0 is 10
At index 1 is 20
At index 2 is 30
At index 3 is 40
At index 4 is 50

除了使用 len() 函數通過索引遍歷數組,還可以使用更方便的 range,結果都是一樣的:

for index,value := range arr {
    fmt.Printf("At index %d is %d\n", index, value)
}

修改數組元素
要修改單個元素的值,直接通過下標訪問元素並賦值就可以了:

arr := [5]int{10,20,30,40,50}
arr[2] = 35

指針數組
數組的元素除了是某個類型外,還可以是某個類型的指針,下面聲明一個所有元素都是指針的數組,然后使用 * 運算符就可以訪問元素指針所指向的值:

arr := [5]*int{0: new(int), 1: new(int)}

new(TYPE) 函數會為一個 TYPE 類型的數據結構划分內存並執行默認的初始化操作,然后返回這個數據對象的指針,所以 new(int) 表示創建一個 int 類型的數據對象,同時返回指向這個對象的指針。

// 為索引為 0 和 1 的元素賦值
*arr[0] = 10
*arr[1] = 20

完成賦值后的結果如下:

我們還可以接着初始化剩下的元素並賦值:

arr[2] = new(int)
arr[3] = new(int)
arr[4] = new(int)
*arr[2] = 30

最后打印整個指針數組指向的內容:

for i := 0; i < len(arr); i++ {
    fmt.Printf("At index %d is %d\n", i, *arr[i])
}

結果如下:

At index 0 is 10
At index 1 is 20
At index 2 is 30
At index 3 is 0
At index 4 is 0

數組是值類型

在 Golang 中,數組是值類型,這意味着數組也可以用在賦值操作中。變量名代表整個數組,同類型的數組可以賦值給另一個數組:

var arr1 [3]string
arr2 := [3]string{"nick", "jack", "mark"}
// 把 arr2 的賦值(其實本質上是復制)到 arr1
arr1 = arr2

復制完成后兩個數組的值完全一樣,但是彼此之間沒有任何關系:

前面我們不止一次地提到:數組的類型包括數組的長度和數組元素的類型。只有這兩部分都一樣才是相同類型的數組,也才能夠互相賦值。下面的代碼中,在類型不同的數組間賦值,編譯器會阻止這樣的操作並報錯:

// 聲明第一個包含 4 個元素的字符串數組
var arr1 [4]string
// 聲明第二個包含 3 個元素的字符串數組,並初始化
arr2 := [3]string{"nick", "jack", "mark"}
// 將 arr2 賦值給 arr1
arr1 = arr2

編譯器表示在賦值時不能把 type [3]string 當 type [4]string 用。

把數組賦值給其它數組時,實際上是完整地復制一個數組。所以,如果數組是一個指針型的數組,那么復制的將是指針,而不會復制指針所指向的對象。看下面的代碼:

// 聲明第一個包含 4 個元素的字符串數組
var arr1 [3]*string
// 聲明第二個包含 3 個元素的字符串數組,並初始化
arr2 := [3]*string{new(string), new(string), new(string)}
*arr2[0] = "nick"
*arr2[1] = "jack"
*arr2[2] = "mark"
// 將 arr2 賦值給 arr1
arr1 = arr2

在賦值完成后,兩個數組指向的是同一組字符串:

把數組傳遞給函數

在 Golang 中數組是一個值類型,所有的值類型變量在賦值和作為參數傳遞時都將產生一次復制操作。如果直接將數組作為函數的參數,則在函數調用時數組會被復制一份傳遞給函數。因此,在函數體中無法修改源數組的內容,因為函數內操作的只是源數組的一個副本。
如此一來,從內存和性能上來看,在函數間傳遞數組是一個開銷很大的操作。因為無論這個數組有多長,都會完整復制,並傳遞給函數。下面的 demo 中會聲明一個包含 100 萬個 int64 類型元素的數組,這會消耗掉 8MB 的內存:

func showArray(array [1e6]int64){
    // do something
}
var arr [1e6]int64
showArray(arr)

每次函數 showArray 被調用時,必須在棧上分配 8MB 的內存。之后整個數組的值(8MB 內存) 被復制到剛剛分配的內存中。雖然 Golang 的運行時會自動處理這個復制操作,但這樣做的效率實在是太低了,也太耗費內存!合理且高效的方式是只傳入指向數組的指針,這樣只需復制 8 個字節的數據到函數的棧上就可以了:

func showArray(array *[1e6]int64){
    // do something
}
var arr [1e6]int64
showArray(&arr)

這段代碼中的 showArray 函數接收一個指向包含 100 萬個 int64 值的數組的指針,調用函數時傳入的參數則是指向數組的指針。現在只需在棧上分配 8 個字節的內存給這個指針就行了。
這個方法能夠更有效地利用內存,性能也更好。需要注意的是,此時在函數內外操作的都是同一個數組中的元素,會互相影響。

多維數組

多維數組的典型用例是平面坐標(二維數組)和三維坐標(三維數組),這里我們簡單介紹一下二維數組。
Golang 的數組本身只有一個維度,但是我們可以組合多個數組從而創建出多維數組,下面是聲明二維數組的實例代碼:

// 聲明一個二維整型數組,兩個維度分別存儲 4 個元素和 2 個元素
var arr [4][2]int
// 使用數組字面量來聲明並初始化一個二維整型數組
arr1 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 聲明並初始化外層數組中索引為 1 和 3 的元素
arr2 := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 聲明並初始化外層數組和內層數組的單個元素
arr3 := [4][2]int{1: {0: 20}, 3: {1: 41}}

下圖展示了上面代碼聲明的二維數組在每次聲明並初始化后包含的值:

為了訪問單個元素,需要反復組合使用 [] 運算符,比如:

arr1[0][0] = 666

因為每個數組都是一個值,所以可以獨立復制某個維度:

// 將 arr1 的索引為 1 的維度復制到一個同類型的新數組里
var arr4 [2]int = arr1[1]
// 將外層數組的索引為 1、內層數組的索引為 0 的整型值復制到新的整型變量里
var value int = arr1[1][0]

總結

數組在 Golang 中是作為高性能的基礎類型設計的,因此對用戶來說使用起來並不是特別方便,這一點在眾多的開源代碼中(數組用的少,slice 用的多)可以得到印證。其實基於數組實現的 slice 以其簡單靈活的特性更易於被大家接受,這也正是 Golang 設計 slice 的初衷。本文介紹了數組這個幕后大英雄,后面的文章會介紹 slice 的用法。

參考:
Golang Array  types
《Go語言編程》
《Go語言實戰》
go基礎系列:數組


免責聲明!

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



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