理解 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)
這段代碼將從切片中取出索引 3
、4
和 5
:
Output["blue coral" "foliose coral" "pillar coral" "leptopsammia" "massive coral" "soft coral"]
現在你知道了如何從一個切片中添加和刪除元素,讓我們來看看如何衡量一個切片所能容納的數據量。
使用 cap()
獲取切片的容量
由於切片的長度是可變的,len()
方法不是確定這種數據類型大小的最佳選擇。相反,你可以使用 cap()
函數來了解一個切片的容量。這將告訴你一個切片可以容納多少個元素,這是由已經爲切片分配的內存的多少決定的。
注意: 由於數組的長度和容量總是相同的,cap()
函數對數組不起作用。
cap()
的一個常見用途是創建一個有預設元素數量的切片,然後再填入元素。這就避免了潛在的不必要的內存分配,因爲使用 append()
來添加超出當前分配容量的元素會導致內存重新分配。
讓我們來看看這樣的場景:我們想做一個數字列表,0
到 3
。我們可以在一個循環中使用 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