1. 程式人生 > >go基礎系列(5):陣列

go基礎系列(5):陣列

瞭解Python、Perl、JavaScript的人想必都知道它們的陣列是動態的,可以隨需求自動增大陣列長度。但Go中的陣列是固定長度的,陣列一經宣告,就無法擴大、縮減陣列的長度。但Go中也有類似的動態"陣列",稱為slice資料結構,在下一篇文章會詳細解釋它。

Go中的陣列是slice和map兩種資料型別的基礎,這兩種資料型別的底層都是通過陣列實現的。

陣列的儲存方式

當在Go中宣告一個數組之後,會在記憶體中開闢一段固定長度的、連續的空間存放陣列中的各個元素,這些元素的資料型別完全相同,可以是內建的簡單資料型別(int、string等),也可以是自定義的struct型別。

  • 固定長度:這意味著陣列不可增長、不可縮減。想要擴充套件陣列,只能建立新陣列,將原陣列的元素複製到新陣列
  • 連續空間:這意味可以在快取中保留的時間更長,搜尋速度更快,是一種非常高效的資料結構,同時還意味著可以通過數值index的方式訪問陣列中的某個元素
  • 資料型別:意味著限制了每個block中可以存放什麼樣的資料,以及每個block可以存放多少位元組的資料

例如,使用下面的語句宣告一個長度為4的int型別的陣列,那麼這個陣列最多隻能存放4個元素,且所有元素都只能是int型別。同時,還為這個陣列做了初始化。

arr_name := [4]int{3,5,22,12}

這個陣列的結構如下圖所示:

其中左上角的小格子中的數表示各元素所在陣列中的位置,也就是它們對應的index,index從0開始計算。

宣告、初始化和訪問陣列

因為Go中的陣列要求資料型別固定、長度固定,所以在宣告的時候需要給定長度和資料型別。

例如宣告一個長度為5、資料型別為int的陣列,名為arr_name。

var arr_name [5]int

必須注意,雖然我們稱呼陣列為int型別的陣列,但每次宣告陣列的時候,陣列的資料型別是兩部分組成的[n]TYPE,這個整體才是陣列的資料型別。所以,[5]int[6]int是兩種不同的陣列型別。不同資料型別,意味著如果陣列賦值給另一陣列時需要資料型別轉換操作,而Go預設是不會進行資料型別轉換的。

在Go中,當一個變數被宣告之後,都會立即對其進行預設的賦0初始化。對int型別的變數會預設初始化為0,對string型別的變數會初始化為空"",對布林型別的變數會初始化為false,對指標(引用)型別的變數會初始化為nil。

陣列也是一種變數型別,也會被初始化。初始化的方式是陣列中的所有元素都根據資料型別賦值0。例如int型別的陣列,元素全部賦值為0,string型別的陣列,元素全部賦值為""等。

所以,上面宣告陣列arr_name之後,它初始化後的結果如下:

可以直接輸出陣列:

import "fmt"
var new_arr [3]int
fmt.Println(new_arr) // 輸出:[0 0 0]

可以將陣列的宣告和初始化為給定值的操作合併:

arr_name := [5]int{3,5,22,12,23}

如果將元素個數指定為特殊符號...,則表示通過初始化時的給定的值個數來推斷陣列長度:

// 宣告長度為3的陣列
arr_name1 := [...]int{2,3,4}

// 宣告長度為4的陣列
arr_name2 := [...]int{2,3,4,5}

如果宣告陣列時,只想給其中某幾個元素初始化賦值,則使用索引號:

arr_name := [5]int{1:10, 2:20}

這表示宣告長度為5的陣列,但第2個元素的值為10,第3個元素的值為20,其它的元素(第1、4、5個元素)都預設初始化為0。

這個陣列聲明後的結果如下:

要訪問陣列中的某個元素,可以使用索引:

arr_name := [5]int{2,3,4,5,6}

// 訪問陣列的第4個元素,將輸出:5
print(arr_name[3])

// 修改陣列第3個元素的值
arr_name[2] = 22

陣列指標(引用)

可以宣告一個指標型別的陣列,這樣陣列中就可以存放指標。注意,指標的預設初始化值為nil。

例如,建立一個指向int型別的陣列:

arr_name := [5]*int{1:new(int), 3:new(int)}

上面的*int表示陣列只能儲存*int型別的資料,也就是指向int的指標型別。new(TYPE)函式會為一個TYPE型別的資料結構劃分記憶體並做預設初始化操作,並返回這個資料物件的指標,所以new(int)表示建立一個int型別的資料物件,同時返回指向這個物件的指標。

初始化後,它的結構如下:注意int指標指向的資料物件會被初始化為0。

對陣列中的指標元素進行賦值:

package main

import "fmt"

func main() {
    arr_name := [5]*int{1:new(int), 3:new(int)}
    *arr_name[1]=10
    *arr_name[3]=30
    
    // 賦值一個新元素
    arr_name[4]=new(int)
    
    fmt.Println(*arr_name[1])
    fmt.Println(*arr_name[3])
    fmt.Println(*arr_name[4])
}

陣列拷貝

在Go中,由於陣列算是一個值型別,所以可以將它賦值給其它陣列。

因為陣列型別的完整定義為[n]TYPE,所以陣列賦值給其它陣列的時候,n和TYPE必須相同。

例如:

// 宣告一個長度為5的string陣列
var str_arr1 [5]string

// 宣告並初始化另一個string陣列
str_arr2 := [5]string{"Perl","Shell","Python","Go","Java"}

// 將str_arr2拷貝給str_arr1
str_arr1 = str_arr2

陣列賦值給其它陣列時,實際上是完整地拷貝一個數組。所以,如果陣列是一個指標型的陣列,那麼拷貝的將是指標陣列,而不會拷貝指標所指向的物件。

package main

import "fmt"

func main() {
    var str_arr1 [3]*string
    str_arr2 := [3]*string{
        new(string),
        new(string),
        new(string),
    }
    *str_arr2[0] = "Perl"
    *str_arr2[1] = "Python"
    *str_arr2[2] = "Shell"
    
    // 陣列賦值,拷貝指標本身,而不拷貝指向的值
    str_arr1 = str_arr2
    
    // 將輸出Python
    fmt.Println(*str_arr1[1])
}

拷貝後,它的結構如下:

傳遞陣列引數給函式

Go中的傳值方式是按值傳遞,這意味著給變數賦值、給函式傳參時,都是直接拷貝一個副本然後將副本賦值給對方的。這樣的拷貝方式意味著:

  • 如果資料結構體積龐大,則要完整拷貝一個數據結構副本時效率會很低
  • 函式內部修改資料結構時,只能在函式內部生效,函式一退出就失效了,因為它修改的是副本物件

陣列同樣也遵循此規則。對於陣列的賦值,上面陣列拷貝中已經解釋過了。如果函式的引數是陣列型別,那麼呼叫函式時傳遞給函式的陣列也一樣是這個陣列拷貝後的一個副本。

例如,建立一個100W個元素的陣列,將其傳遞給函式foo():

var big_arr [1e6]int

func foo(a [1e6]int) {
    ...
}

// 呼叫foo
foo(bigarr)

當上面宣告big_arr後,就有100W個元素,假設這個int佔用8位元組,整個陣列就佔用800W位元組,大約有8M資料。當呼叫foo的時候,Go會直接複製這8M資料形成另一個數組副本,並將這個副本交給foo進行處理。在foo中處理的陣列,實際上是這個副本,foo()不會對原始的big_arr產生任何影響。

可以將陣列的指標傳遞給函式,這樣指標傳遞給函式時,複製給函式引數的是這個指標,總共才8個位元組(每個指標佔用8位元組),複製的資料量非常少。而且,因為複製的是指標,foo()修改這個陣列時,會直接影響原始陣列。

var big_arr [1e6]int

// 生成陣列的指標
ref_big_arr := &big_arr

func foo(ra *[1e6]int) {
    ...
}

// 呼叫foo,傳遞指標
foo(ref_big_arr)

多維陣列

可以通過組合兩個一維陣列的方式構成二維陣列。一般在處理具有父、子關係或者有座標關係的資料時,二維陣列比較有用。

例如,宣告二維陣列:

var t_arr [4][2]int

這表示陣列有4個元素,每個元素都是一個包含2元素的小陣列。換一種方式,例如:

t_arr := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

還可以指定位置進行初始化:

t_arr := [4][2]int{1: {20, 21}, 3: {40, 41}}
t_arr := [4][2]int{1: {0: 20}, 3: {1: 41}}