理解 Go 中的數組和切片

介紹

在 Go 中,數組和切片是數據結構,由元素的有序序列組成。當你想處理許多相關的值時,這些數據集合是很好的選擇。它們可以讓你把相同類型的數據放在一起,降低代碼量,並同時對多個值執行相同的方法和操作。

雖然 Go 中的數組和切片都是元素的有序序列,但兩者之間有很大的區別。Go 中的數組是一個數據結構,由一個有序的元素序列組成,其容量在創建時已經定義。一旦數組分配了它的大小,就不能再改變它的大小。另一方面,切片是數組的可變長度版本,爲使用這些數據結構的開發者提供更多的靈活性。切片其實是你在其他語言中認爲的數組。

鑑於這些差異,在一些特定的情況下,你會選擇使用一個替代另一個。如果你是 Go 的新手,確定何時使用它們可能會讓人困惑。儘管切片的多功能性使其在大多數情況下成爲更合適的選擇,但在一些特殊情況下,數組可以優化程序的性能。

本文將詳細介紹數組和切片,以便在這些數據類型之間做出適當的選擇。此外,你將回顧聲明和處理數組和切片的常用方法。本教程將首先提供對數組的描述以及如何操作它們,然後解釋切片以及它們的區別。

數組

數組是具有固定數量元素的數據結構。因爲數組的大小是靜態的,所以數據結構只需要分配一次內存,而可變長度的數據結構則必須動態地分配內存,以便將來可以變大或變小。儘管數組的固定長度會使它們在工作時有些僵硬,但一次性的內存分配可以提高程序的速度和性能。正因爲如此,開發人員在優化程序時通常使用數組,在這種情況下,數據結構不需要可變數量的元素。

定義一個數組

數組的定義是在大括號 [] 中聲明數組的大小,然後是各元素的數據類型。Go 中的數組必須使其所有元素都是相同的 [數據類型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})。在數據類型之後,你可以用大括號 { } 來聲明數組元素的單個值。

下面是聲明一個數組的一般模式:

[capacity]data_type{element_values}

注意: 重要的是要記住,每一個新數組的聲明都會創建一個不同的類型。所以,儘管 [2]int[3]int 都有整數元素,但它們不同的長度使得它們的數據類型不兼容。

如果你不聲明數組元素的值,默認爲零值,這意味着數組的元素將是空的。對於整數,用 0 表示,對於字符串,用空字符串表示。

例如,下面的數組 numbers 有三個整數元素,但還沒有值:

var numbers [3]int

如果你打印 numbers,會得到以下輸出:

Output
[0 0 0]

如果你想在創建數組時指定元素的值,需要將這些值放在大括號裏。一個有設定值的字符串數組看起來像這樣:

[4]string{"blue coral""staghorn coral""pillar coral""elkhorn coral"}

可以將一個數組存儲在一個變量中並打印出來:

coral := [4]string{"blue coral""staghorn coral""pillar coral""elkhorn coral"}
fmt.Println(coral)

打印結果如下:

Output
[blue coral staghorn coral pillar coral elkhorn coral]

請注意,當數組被打印出來時,數組中的元素之間沒有劃分,因此很難分辨一個元素在哪裏結束,另一個元素在哪裏開始。由於這個原因,有時使用 fmt.Printf 函數是很有幫助的,它可以在打印到屏幕前對字符串進行格式化。在該命令中提供 %q 謂詞並加上引號:

fmt.Printf("%q\n", coral)

打印的結果如下:

Output
["blue coral" "staghorn coral" "pillar coral" "elkhorn coral"]

現在每個元素都有引號。\n 謂詞指示格式化會在每行結束打印換行符。

有了關於如何聲明數組及其組成的概念,現在可以繼續學習如何用索引號指定數組中的元素。

索引數組和切片

數組中的每個元素(也包括切片)都可以通過索引單獨調用。每個元素都對應着一個索引號,它是一個從索引號 0 開始向上計數的 int 值。

在下面的例子中,我們將使用一個數組,但是你也可以使用一個切片,因爲它們在索引方式上是相同的。

對於數組 coral 來說,索引看起來像這樣。

“blue coral” “staghorn coral” “pillar coral” “elkhorn coral”

0 1 2 3

第一個元素,字符串 "blue coral",從索引 0 開始,在索引爲 3 的元素 "elkhorn coral"結束。

因爲切片或數組中的每個元素都有一個相應的索引號,我們能夠以與其他順序數據類型相同的方式訪問和操作它們。

現在我們可以通過引用索引號來調用切片 中的一個離散元素:

fmt.Println(coral[1])
Output
staghorn coral

這個切片的索引號範圍是 0-3,如前表所示。所以要單獨調用一個元素的話,我們要像這樣引用索引號:

coral[0] = "blue coral"
coral[1] = "staghorn coral"
coral[2] = "pillar coral"
coral[3] = "elkhorn coral"

我們調用數組 coral 的索引號如果大於 3,它就會超出範圍並 panic,因爲它是無效的:

fmt.Println(coral[18])
Output
panic: runtime error: index out of range

當對數組或切片進行索引時,你必須始終使用正數。不像有些語言允許你用負數進行反向索引,在 Go 中這樣做會導致錯誤:

fmt.Println(coral[-1])
Output
invalid array index -1 (index must be non-negative)

我們可以使用 + 操作符將數組或切片中的字符串元素與其他字符串連接起來:

fmt.Println("Sammy loves " + coral[0])
Output
Sammy loves blue coral

我們能夠將索引號爲 0 的字符串元素與 "Sammy loves" 的字符串連接起來。

有了與數組或切片中的元素相對應的索引號,我們就能夠單獨訪問每個元素,並對這些元素進行操作。爲了證明這一點,我們接下來將看看如何修改某個索引的元素。

修改元素

我們可以使用索引號來給元素賦值來改變數組或切片中的元素。這使我們對切片和數組中的數據有了更大的控制,並將允許我們以編程方式操作單個元素。

如果我們想把數組 coral 中索引爲 1 的元素的字符串值從 "staghorn coral" 改爲 "foliose coral",我們可以這樣做:

coral[1] = "foliose coral"

現在,當我們打印 coral 時,數組將是不同的:

fmt.Printf("%q\n", coral)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral"]

現在你知道了如何操作數組或切片的單個元素,讓我們來看看幾個函數,它們將在處理集合數據類型時給你更多的靈活性。

使用 len 計算元素個數

在 Go 中,len() 是一個內置函數,用來幫助你處理數組和切片。像處理字符串一樣,你可以用數組或切片作爲參數通過使用 len() 來計算一個數組或切片的長度。

例如,要想知道 coral 數組中有多少個元素,你可以使用:

len(coral)

如果你打印出數組 coral 的長度,你會收到以下輸出:

Output
4

可以看到數據類型爲 int 的數組長度爲 4,這是正確的,因爲數組 coral 有四個元素:

coral := [4]string{"blue coral""foliose coral""pillar coral""elkhorn coral"}

如果你創建一個有更多元素的整數數組,你也可以對其使用 len() 函數:

numbers := [13]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
fmt.Println(len(numbers))

得到的輸出如下:

Output
13

儘管這些例子數組的元素相對較少,但在元素非常多的數組中 len() 函數特別有用。

接下來,我們將介紹如何向集合數據類型添加一個元素,並展示由於數組的固定長度,追加這些靜態數據類型將導致錯誤。

使用 append() 追加元素

append() 是 Go 中的一個內置方法,可以向集合數據類型添加元素。然而,當這個方法用於數組時,將無法工作。如前所述,數組與切片不同的是數組的大小不能被修改。這意味着,雖然你可以改變數組中元素的值,但在定義了數組後,你不能使其變大或變小。

讓我們看看你的 coral 數組:

coral := [4]string{"blue coral""foliose coral""pillar coral""elkhorn coral"}

假設你想把 "black coral" 這個元素添加到這個數組中。如果你試圖用 append() 函數在數組中輸入:

coral = append(coral, "black coral")

你將得到一個錯誤的輸出:

Output
first argument to append must be slice; have [4]string

爲了解決這個問題,我們進一步瞭解切片的數據類型,如何定義一個切片,以及如何從數組轉換爲切片。

切片

切片是 Go 中的一種數據類型,它是一個可變的數據結構,有序的元素序列。由於切片的大小是可變的,所以在使用時有更大的靈活性;當處理可能需要擴容或縮容的數據集合時,使用切片可以確保你的代碼在操作集合的長度時不會出現錯誤。在大多數情況下,與數組相比,這種可變性值得切片有時可能需要內存重新分配。當你需要存儲大量的元素或對元素進行迭代,並且你希望能夠隨時修改這些元素時,切片數據類型是你的首選。

定義一個切片

切片的定義是通過聲明數據類型,前面是一組空的方括號([])和大括號之間的元素列表({})。你會注意到,與需要在大括號之間加入 int 來聲明特定長度的數組不同,一個切片在大括號之間沒有任何東西來代表其可變長度。

我們來創建一個包含字符串數據類型元素的切片:

seaCreatures := []string{"shark""cuttlefish""squid""mantis shrimp""anemone"}

當我們打印出切片時,就可以看到切片中的元素:

fmt.Printf("%q\n", seaCreatures)

可以得到如下的輸出:

Output
["shark" "cuttlefish" "squid" "mantis shrimp" "anemone"]

如果你想在不填充元素的情況下創建一個特定長度的切片,你可以使用內置的 make() 函數:

oceans := make([]string, 3)

打印切片將得到如下輸出:

Output
["" "" ""]

如果你想預先分配一定容量的內存,你可以向 make() 傳入第三個參數:

oceans := make([]string, 3, 5)

這將給分配一個長度爲 3 容量爲 5 的零切片。

你現在知道如何聲明一個切片了。然而,這還沒有解決我們之前在 coral 數組中遇到的錯誤。要在 coral 中使用 append() 函數,你首先要學會如何在一個數組中切出部分內容。

將數組切片

通過使用索引號來確定開始和結束點,你可以調用一個數組內的一個分片值。這被稱爲對數組進行切分,你可以通過創建一個由冒號分隔的索引號範圍來實現,其形式爲 [first_index:second_index]。然而,需要注意的是,當對一個數組進行切分時,其結果是一個切片,而不是一個數組。

假設你想只打印 coral 數組的中間項,而不打印第一個和最後一個元素。你可以通過創建一個從索引 1 開始,在索引 3 之前結束的分片來實現:

fmt.Println(coral[1:3])

運行程序會產生以下結果:

Output
[foliose coral pillar coral]

當創建一個切片時,如 [1:3],第一個數字是切片開始的地方(包含),第二個數字是第一個數字和你想檢索的元素總數之和:

array[starting_index : (starting_index + length_of_slice)]

在這個例子中,你調用第二個元素(或索引 1)作爲起點,總共調用了兩個元素。計算結果會是這樣的:

array[1 : (1 + 2)]

所以我們可以得到下面的代碼:

coral[1:3]

如果你想把數組的開始或結束作爲切片的起點或終點,你可以省略 array[first_index:second_index] 語法中的一個數字。例如,如果你想打印數組 coral 的前三個元素 -- 即 "blue coral""foliose coral""pillar coral" -- 你可以這麼做:

fmt.Println(coral[:3])

將輸出:

Output
[blue coral foliose coral pillar coral]

這就打印了數組的開頭,在索引 3 之前就停止了。

如果要包括數組末尾的所有元素,你可以反過來使用這個語法:

fmt.Println(coral[1:])

這將得到以下切片:

Output
[foliose coral pillar coral elkhorn coral]

本節討論了通過分片來調用數組的各個部分。接下來,你將學習如何使用分片將整個數組轉換爲切片。

數組轉換爲切片

如果你創建了一個數組,並決定需要它有一個可變的長度,你可以把它轉換爲一個切片。要將一個數組轉換爲切片,使用你在本教程的將數組切片步驟中學到的切片過程,只是這次要省略決定端點的兩個索引號,從而選擇整個切片:

coral[:]

請記住,你不能將變量 coral 本身轉換爲一個切片,因爲一旦在 Go 中定義了一個變量,它的類型就不能被改變。爲了解決這個問題,你可以把數組的全部內容複製到一個新的變量中作爲一個切片:

coralSlice := coral[:]

如果你打印 coralSlice,你將得到以下輸出:

Output
[blue coral foliose coral pillar coral elkhorn coral]

現在,使用 append() 在新切片中添加 black coral 元素:

coralSlice = append(coralSlice, "black coral")
fmt.Printf("%q\n", coralSlice)

這將輸出添加了新元素的切片:

Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral"]

我們也可以在一條 append() 語句中添加一個以上的元素:

coralSlice = append(coralSlice, "antipathes""leptopsammia")
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia"]

要將兩個切片組合在一起,可以使用 append(),但必須使用 ... 的擴展語法來擴展第二個參數進行追加。

moreCoral := []string{"massive coral""soft coral"}
coralSlice = append(coralSlice, moreCoral...)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

現在你已經學會了如何將一個元素追加到你的切片中,我們再來看看如何刪除一個元素。

從切片中刪除元素

與其他語言不同,Go 沒有提供任何內置函數來從切片中刪除元素。需要通過分片的方式從切片中刪除元素。

要刪除一個元素,你必須切出該元素之前的元素,切出該元素之後的元素,然後將這兩個新的切片加在一起,不包括你想刪除的元素。

如果 i 是要刪除的元素的索引,那麼這個過程的格式看起來就像下面這樣:

slice = append(slice[:i], slice[i+1:]...)

coralSlice 中,讓我們刪除 "elkhorn coral" 這個元素。它的索引爲 3

coralSlice := []string{"blue coral""foliose coral""pillar coral""elkhorn coral""black coral""antipathes""leptopsammia""massive coral""soft coral"}
    
coralSlice = append(coralSlice[:3], coralSlice[4:]...)
    
fmt.Printf("%q\n", coralSlice)
Output["blue coral" "foliose coral" "pillar coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

現在索引位置 3 的元素 "elkhorn coral",已經不在我們的切片 coralSlice 中了。

我們也可以用同樣的方法刪除一個範圍。假設我們不僅想刪除 "elkhorn coral" 這一項,還想刪除 "black coral""antipathes"。我們可以在表達式中使用一個範圍來完成這個任務:

coralSlice := []string{"blue coral""foliose coral""pillar coral""elkhorn coral""black coral""antipathes""leptopsammia""massive coral""soft coral"}
    
coralSlice = append(coralSlice[:3], coralSlice[6:]...)
    
fmt.Printf("%q\n", coralSlice)

這段代碼將從切片中取出索引 345

Output["blue coral" "foliose coral" "pillar coral" "leptopsammia" "massive coral" "soft coral"]

現在你知道了如何從一個切片中添加和刪除元素,讓我們來看看如何衡量一個切片所能容納的數據量。

使用 cap() 獲取切片的容量

由於切片的長度是可變的,len() 方法不是確定這種數據類型大小的最佳選擇。相反,你可以使用 cap() 函數來了解一個切片的容量。這將告訴你一個切片可以容納多少個元素,這是由已經爲切片分配的內存的多少決定的。

注意: 由於數組的長度和容量總是相同的,cap()函數對數組不起作用。

cap() 的一個常見用途是創建一個有預設元素數量的切片,然後再填入元素。這就避免了潛在的不必要的內存分配,因爲使用 append() 來添加超出當前分配容量的元素會導致內存重新分配。

讓我們來看看這樣的場景:我們想做一個數字列表,03。我們可以在一個循環中使用 append() 來完成,或者我們可以先預分配切片,然後使用 cap() 來循環填充這些數值。

首先,我們可以看看使用append()

numbers := []int{}
for i := 0; i < 4; i++ {
    numbers = append(numbers, i)
}
fmt.Println(numbers)
Output
[0 1 2 3]

在這個例子中,我們創建了一個切片,然後創建了一個 for 循環,這個循環會迭代四次。每次迭代都將循環變量 i 的當前值追加到 numbers 切片的索引中。然而,這可能會導致不必要的內存分配,使你的程序變慢。當添加到一個空切片時,每次你調用 append 時,程序都會檢查切片的容量。如果添加的元素超過了這個容量,程序將分配額外的內存來存儲它。這在你的程序中產生了額外的開銷,並可能導致執行速度變慢。

現在讓我們在不使用 append() 的情況下,通過預先分配一定的長度 / 容量來填充切片:

numbers := make([]int, 4)
for i := 0; i < cap(numbers); i++ {
    numbers[i] = i
}
    
fmt.Println(numbers)
Output
[0 1 2 3]

在這個例子中,我們使用 make() 來創建一個切片,並讓它預先分配 4 個元素。然後我們在循環中使用 cap() 函數來迭代每個歸零的元素,填充每個元素直到達到預先分配的容量。在每個循環中,我們將循環變量 i 的當前值放入 numbers 的索引中。

雖然 append()cap() 在功能上是等同的,但 cap() 的例子避免了使用 append() 函數所需的額外的內存分配。

構建多維切片

你也可以定義由切片作爲元素的切片,每個括號內的列表都包含在父切片的大括號內。像這樣的切片集合被稱爲多維切片。這些可以被認爲是描述多維座標的;例如,一個由五個切片組成的集合,每個切片有六個元素,可以代表一個水平長度爲 5、垂直高度爲 6 的二維網格。

讓我們來看看下面這個多維切片:

seaNames := [][]string{{"shark""octopus""squid""mantis shrimp"}{"Sammy""Jesse""Drew""Jamie"}}

要訪問這個切片中的一個元素,我們必須使用多個索引,結構的每個維度都有一個索引:

fmt.Println(seaNames[1][0])
fmt.Println(seaNames[0][0])

在前面的代碼中,我們首先打印索引爲 1 0 的元素,然後我們打印索引爲 0 0 的元素。這將產生以下結果:

OutputSammy
shark

以下是其餘各個元素的索引值:

seaNames[0][0] = "shark"
seaNames[0][1] = "octopus"
seaNames[0][2] = "squid"
seaNames[0][3] = "mantis shrimp"
    
seaNames[1][0] = "Sammy"
seaNames[1][1] = "Jesse"
seaNames[1][2] = "Drew"
seaNames[1][3] = "Jamie"

在處理多維切片時,重要的是要記住,你需要參考一個以上的索引號,以便訪問嵌套切片中的特定元素。

總結

在本教程中,你學到了在 Go 中使用數組和切片的基礎。通過多個練習來證明數組的長度是固定的,而切片的長度是可變的,並發現這種差異是如何影響這些數據結構的用途場景。

要繼續學習 Go 中的數據結構,請查看我們的文章**《理解 Go 中的 Map》**,或探索整個如何在 Go 中編碼系列。

相關鏈接:

數據結構:https://en.wikipedia.org/wiki/Data_structure

理解 Go 中的 Map**:**_docs/15-Understanding_Maps_in_Go.md_

如何在 Go 中編碼:https://gocn.github.io/How-To-Code-in-Go/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9HBLixdXnWFhmkoTI2wCJw