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: %dn", cap(s), len(s))
s = append(s, -100)
fmt.Printf("After. Cap: %d, length: %dn", 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
的源代碼。
如果你有任何問題或反饋,請在下方發表評論或在 Twitter 上與我聯繫。
via: https://opensource.com/article/18/7/introduction-go-arrays-and-slices
作者:Mihalis Tsoukalos 選題:lkxed 譯者:lkxed 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive