Go 數組和切片的介紹
了解使用數組和切片在 Go 中存儲數據的優缺點,以及為什么其中一個更好。
在本文中,我將解釋 Go 數組和切片,包括如何使用它們,以及為什么你通常要選擇其中一個而不是另一個。
數組
數組是編程語言中最流行的數據結構之一,主要原因有兩個:一是簡單易懂,二是可以存儲許多不同類型的數據。
你可以聲明一個名為 anArray 的 Go 數組,該數組存儲四個整數,如下所示:
anArray := [4]int{-1, 2, 0, -4}
數組的大小應該在它的類型之前聲明,而類型應該在聲明元素之前定義。len() 函數可以幫助你得到任何數組的長度。上面數組的大小是 4。
如果你熟悉其他編程語言,你可能會嘗試使用 for 循環來遍歷數組。Go 當然也支持 for 循環,不過,正如你將在下面看到的,Go 的 range 關鍵字可以讓你更優雅地遍歷數組或切片。
最后,你也可以定義一個二維數組,如下:
twoD := [3][3]int{
{1, 2, 3},
{6, 7, 8},
{10, 11, 12}}
arrays.go 源文件中包含了 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 中每個切片都有一個底層數組。
切片具有 capacity 屬性和 length 屬性,它們并不總是相同的。切片的長度與元素個數相同的數組的長度相同,可以使用 len() 函數得到。切片的容量是當前為切片分配的空間,可以使用 cap() 函數得到。
由于切片的大小是動態的,如果切片空間不足(也就是說,當你嘗試再向切片中添加一個元素時,底層數組的長度恰好與容量相等),Go 會自動將它的當前容量加倍,使其空間能夠容納更多元素,然后將請求的元素添加到底層數組中。
此外,切片是通過引用傳遞給函數的,這意味著實際傳遞給函數的是切片變量的內存地址,這樣一來,你對函數內部的切片所做的任何修改,都不會在函數退出后丟失。因此,將大切片傳遞給函數,要比將具有相同數量元素的數組傳遞給同一函數快得多。這是因為 Go 不必拷貝切片 —— 它只需傳遞切片變量的內存地址。
slice.go 源文件中有 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(int 類型的零值)。
使用切片來引用數組
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 數組更加通用、強大。只有少數情況需要使用數組而不是切片,特別是當你完全確定元素的數量固定不變時。
你可以在 GitHub 上找到 arrays.go、slice.go 和 refArray.go 的源代碼。