介紹 Go 的陣列和切片

摸鱻發表於2019-11-04

學習在 Go 中使用陣列和切片儲存資料的優缺點,以及為什麼其中一個比另一個更好

Traffic lights at night

圖片來自於: 
carrotmadman6. Modified by Opensource.com. CC BY-SA 2.0

陣列

陣列是程式語言中最流行的資料結構之一,主要有兩個原因:它們簡單易懂,並且可以儲存許多不同型別的資料。

你可以宣告一個名為 anArray 的 Go 陣列,它儲存了四個整數:

anArray := [4]int{-1, 2, 0, -4}

陣列的大小應該在型別之前宣告,型別應該在元素之前定義。len() 函式可以幫助你找出任何陣列的長度。上面的陣列的大小是 4。

如果您熟悉其他程式語言,您可能會嘗試使用 for 迴圈訪問陣列中的所有元素。然而,正如您將在下面看到的,Go 的range 關鍵字允許您以一種更優雅的方式訪問陣列或片中的所有元素。

最後,介紹如何定義二維陣列:

twoD := [3][3]int{
    {1, 2, 3},
    {6, 7, 8},
    {10, 11, 12}}

arrays.go 原始檔解釋瞭如何使用 Go 的陣列。arrays.go 最重要的程式碼是:

for i := 0; i < len(twoD); i++ {
        k := twoD[i]
        for j := 0; j < len(k); j++ {
                fmt.Print(k[j], " ")
        }
        fmt.Println()
}

for _, a := range twoD {
        for _, j := range a {
                fmt.Print(j, " ")
        }
        fmt.Println()
}

這展示瞭如何使用 for 迴圈和 range 關鍵字遍歷陣列的元素。arrays.go 的其餘程式碼。展示瞭如何將陣列作為引數傳遞到函式中。

下面是 arrays.go 的輸出:

$ go run arrays.go
Before change(): [-1 2 0 -4]
After change(): [-1 2 0 -4]
1 2 3
6 7 8
10 11 12
1 2 3
6 7 8
10 11 12

這個輸出說明,在函式退出後,您對函式內的陣列所做的更改將丟失。

陣列的缺點

Go 陣列有很多缺點,這將使您重新考慮在 Go 專案中使用它們。首先,你不能在定義一個陣列後改變它的大小,這意味著Go 陣列不是動態的。簡單地說,如果需要將一個元素新增到一個沒有任何剩餘空間的陣列中,則需要建立一個更大的陣列,並將舊陣列中的所有元素複製到新陣列中。其次,當您將陣列作為引數傳遞給函式時,實際上傳遞的是陣列的一個副本,這意味著您對函式內部陣列所做的任何更改都將在函式退出後丟失。最後,將一個大陣列傳遞給一個函式可能非常慢,這主要是因為 Go 必須建立一個陣列的副本。

Go 切片可以解決所有的問題。

切片

Go 切片類似於沒有缺點的 Go 陣列。首先,可以使用 append() 函式向現有切片新增一個元素。此外,Go 切片是使用陣列在內部實現的,這意味著 Go 對每個切片使用一個底層陣列。

切片具有一個 容量 屬性和一個 長度 屬性,它們並不總是相同的。切片的長度與元素數量相同的陣列的長度相同,可以使用 len() 函式找到它。切片的容量是當前分配給切片的空間,可以使用 cap() 函式找到它。

切片的大小是動態的,如果切片的容量耗盡(這意味著當您試圖向陣列新增另一個元素時,陣列的當前長度與它的容量相同), Go 會自動將當前容量加倍,為更多元素騰出空間,並將請求的元素新增到陣列中。

此外,切片是通過對函式的引用傳遞的,這意味著實際傳遞給函式的是切片變數的記憶體地址,在函式退出後,對函式內部切片的任何修改都不會丟失。因此,將一個大的切片傳遞給一個函式要比將一個具有相同數量元素的陣列傳遞給同一個函式要快得多。這是因為 Go 不必複製切片——它只傳遞切片變數的記憶體地址。

Go 切片在 slice.go 中有說明,包含以下程式碼:

package main

import (
        "fmt"
)

func negative(x []int) {
        for i, k := range x {
                x[i] = -k
        }
}

func printSlice(x []int) {
        for _, number := range x {
                fmt.Printf("%d ", number)
        }
        fmt.Println()
}

func main() {
        s := []int{0, 14, 5, 0, 7, 19}
        printSlice(s)
        negative(s)
        printSlice(s)

        fmt.Printf("Before. Cap: %d, length: %d\n", cap(s), len(s))
        s = append(s, -100)
        fmt.Printf("After. Cap: %d, length: %d\n", cap(s), len(s))
        printSlice(s)

        anotherSlice := make([]int, 4)
        fmt.Printf("A new slice with 4 elements: ")
        printSlice(anotherSlice)
}

切片定義和陣列定義之間最大的區別是,您不需要指定切片的大小,切片的大小是由您想要放入其中的元素數量決定的。 另外,append() 函式允許您向現有的切片新增一個元素——注意,即使切片的容量允許您向該切片新增一個元素,它的長度也不會改變,除非您呼叫 append()printSlice() 函式是一個輔助函式,用於列印其切片引數的元素,而 negative()函式則處理其切片引數的所有元素。

slice.go 的輸出是:

$ go run slice.go
0 14 5 0 7 19
0 -14 -5 0 -7 -19
Before. Cap: 6, length: 6
After. Cap: 12, length: 7
0 -14 -5 0 -7 -19 -100
A new slice with 4 elements: 0 0 0 0

請注意,當您建立一個新的切片併為給定數量的元素分配記憶體空間時,Go 將自動將所有元素初始化為其型別的 0 值,在本例中為 0。

切片中引用陣列

Go 允許您使用 [:] 符號引用一個帶有切片的現有陣列。在這種情況下,您對切片函式所做的任何更改都將傳播到陣列中——這在 refArray.go 中得到了說明。請記住,[:] 符號並不建立陣列的副本,只是對它的引用。

refArray.go 最有趣的部分是:

func main() {
        anArray := [5]int{-1, 2, -3, 4, -5}
        refAnArray := anArray[:]

        fmt.Println("Array:", anArray)
        printSlice(refAnArray)
        negative(refAnArray)
        fmt.Println("Array:", anArray)
}

refArray.go 輸出是:

$ go run refArray.go
Array: [-1 2 -3 4 -5]
-1 2 -3 4 -5
Array: [1 -2 3 -4 5]

因此,anArray 陣列的元素會因為切片對它的引用而改變。

總結

儘管 Go 同時支援陣列和切片,但你現在應該很清楚,您很可能會使用切片,因為它們比 Go 陣列更通用、更強大。只有少數情況下需要使用陣列而不是切片。最明顯的一種情況是,您完全確定需要儲存固定數量的元素。

你可以找到本文的 Go 程式碼 arrays.goslice.go, 和 refArray.go 在 GitHub.

譯自opensource

最初的時候也是最苦的時候,最苦的時候也是最酷的時候。

相關文章