聊聊Go語言中的陣列與切片

ITPUB社群 發表於 2022-12-01
Go

1. 陣列

陣列是一個由固定長度的特定型別元素組成的序列,一個陣列可以由零個或多個元素組成。因為陣列的長度是固定的,因此在 Go 語言中很少直接使用陣列。和陣列對應的型別是 Slice(切片),它是可以增長和收縮的動態序列,slice 功能也更靈活。
陣列的每個元素可以透過索引下標來訪問,索引下標的範圍是從 0 開始到陣列長度減 1 的位置。內建的 len 函式將返回陣列中元素的個數。




var a [3]int             // array of 3 integersfmt.Println(a[0])        // print the first elementfmt.Println(a[len(a)-1]) // print the last element, a[2]

預設情況下,陣列的每個元素都被初始化為元素型別對應的零值,對於數字型別來說就是 0。




var q [3]int = [3]int{1, 2, 3}var r [3]int = [3]int{1, 2}fmt.Println(r[2]) // "0"

如果在陣列的長度位置出現的是“...”省略號,則表示陣列的長度是根據初始化值的個數來計算。因此,上面 q 陣列的定義可以簡化為:

q := [...]int{1, 2, 3}fmt.Printf("%T\n", q) // "[3]int"

陣列的長度是陣列型別的一個組成部分,因此[3]int 和[4]int 是兩種不同的陣列型別。

陣列的長度必須是常量表示式,因為陣列的長度需要在編譯階段確定。


q := [3]int{1, 2, 3}q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int

如果一個陣列的元素型別是可以相互比較的,那麼陣列型別也是可以相互比較的,這時候我們可以直接透過==比較運算子來比較兩個陣列,只有當兩個陣列的所有元素都是相等的時候陣列才是相等的。不相等比較運算子!=遵循同樣的規則。







a := [2]int{1, 2}b := [...]int{1, 2}c := [2]int{1, 3}fmt.Println(a == b, a == c, b == c) // "true false false"d := [3]int{1, 2}fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

2. 切片(Slice)

Slice(切片)代表變長的序列,序列中每個元素都有相同的型別。一個 slice 型別一般寫作[]T,其中 T 代表 slice 中元素的型別;slice 的語法和陣列很像,只是沒有固定長度而已。
一個 slice 是一個輕量級的資料結構,提供了訪問陣列子序列(或者全部)元素的功能,而且 slice 的底層確實引用一個陣列物件。
一個 slice 由三個部分構成:指標、長度和容量。

  • 指標指向第一個 slice 元素對應的底層陣列元素的地址,要注意的是 slice 的第一個元素並不一定就是陣列的第一個元素。
  • 長度對應 slice 中元素的數目;
  • 長度不能超過容量,容量一般是從 slice 的開始位置到底層資料的結尾位置。內建的 len 和 cap 函式分別返回 slice 的長度和容量。


表示一年中每個月份名字的字串陣列,還有重疊引用了該陣列的兩個 slice。陣列這樣定義:


months := [...]string{1: "January", /* ... */, 12: "December"}

因此一月份是 months[1],十二月份是 months[12]。

通常,陣列的第一個元素從索引 0 開始,但是月份一般是從 1 開始的,因此我們宣告陣列時直接跳過第 0 個元素,第 0 個元素會被自動初始化為空字串。


slice 的切片操作 s[i:j],其中 0 ≤ i≤ j≤ cap(s),用於建立一個新的 slice,引用 s 的從第 i 個元素開始到第 j-1 個元素的子序列。新的 slice 將只有 j-i 個元素。如果 i 位置的索引被省略的話將使用 0 代替,如果 j 位置的索引被省略的話將使用 len(s)代替。因此,months[1:13]切片操作將引用全部有效的月份,和 months[1:]操作等價;months[:]切片操作則是引用整個陣列。讓我們分別定義表示第二季度和北方夏天月份的 slice,它們有重疊部分:
聊聊Go語言中的陣列與切片





Q2 := months[4:7]summer := months[6:9]fmt.Println(Q2)     // ["April" "May" "June"]fmt.Println(summer) // ["June" "July" "August"]
兩個 slice 都包含了六月份。

append 函式

append 函式用於向 slice 追加元素:






var runes []runefor _, r := range "Hello, 世界" {    runes = append(runes, r)}fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"

為了提高記憶體使用效率,新分配的陣列一般略大於儲存 x 和 y 所需要的最低大小。透過在每次擴充套件陣列時直接將長度翻倍從而避免了多次記憶體分配,也確保了新增單個元素操作的平均時間是一個常數時間。這個程式演示了效果:





















func main() {    var x, y []int    for i := 0; i < 10; i++ {        y = appendInt(x, i)        fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)        x = y    }}
//每一次容量的變化都會導致重新分配記憶體和copy操作:0  cap=1    [0]1  cap=2    [0 1]2  cap=4    [0 1 2]3  cap=4    [0 1 2 3]4  cap=8    [0 1 2 3 4]5  cap=8    [0 1 2 3 4 5]6  cap=8    [0 1 2 3 4 5 6]7  cap=8    [0 1 2 3 4 5 6 7]8  cap=16   [0 1 2 3 4 5 6 7 8]9  cap=16   [0 1 2 3 4 5 6 7 8 9]

讓我們仔細檢視 i=3 次的迭代。當時 x 包含了[0 1 2]三個元素,但是容量是 4,因此可以簡單將新的元素新增到末尾,不需要新的記憶體分配。然後新的 y 的長度和容量都是 4,並且和 x 引用著相同的底層陣列,如圖 4.2 所示。
聊聊Go語言中的陣列與切片
在下一次迭代時 i=4,現在沒有新的空餘的空間了,因此 appendInt 函式分配一個容量為 8 的底層陣列,將 x 的 4 個元素[0 1 2 3]複製到新空間的開頭,然後新增新的元素 i,新元素的值是 4。新的 y 的長度是 5,容量是 8;後面有 3 個空閒的位置,三次迭代都不需要分配新的空間。當前迭代中,y 和 x 是對應不同底層陣列的 view。這次操作如圖 4.3 所示。
聊聊Go語言中的陣列與切片

內建的 append 函式可能使用比 appendInt 更復雜的記憶體擴充套件策略。
因此,通常我們並不知道 append 呼叫是否導致了記憶體的重新分配,因此我們也不能確認新的 slice 和原始的 slice 是否引用的是相同的底層陣列空間。
同樣,我們不能確認在原先的 slice 上的操作是否會影響到新的 slice。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2926164/,如需轉載,請註明出處,否則將追究法律責任。