Linux中國

Go 數組和切片的介紹

了解使用數組切片Go 中存儲數據的優缺點,以及為什麼其中一個更好。

在本系列的第四篇文章中,我將解釋 Go 數組切片,包括如何使用它們,以及為什麼你通常要選擇其中一個而不是另一個。

數組

數組是編程語言中最流行的數據結構之一,主要原因有兩個:一是簡單易懂,二是可以存儲許多不同類型的數據。

你可以聲明一個名為 anArrayGo 數組,該數組存儲四個整數,如下所示:

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.goslice.gorefArray.go 的源代碼。

如果你有任何問題或反饋,請在下方發表評論或在 Twitter 上與我聯繫。

via: https://opensource.com/article/18/7/introduction-go-arrays-and-slices

作者:Mihalis Tsoukalos 選題:lkxed 譯者:lkxed 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國