聊聊 Go 語言中的數組與切片

這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰程序員

1. 數組

數組是一個由固定長度的特定類型元素組成的序列,一個數組能夠由零個或多個元素組成。由於數組的長度是固定的,所以在 Go 語言中不多直接使用數組。和數組對應的類型是 Slice(切片),它是能夠增加和收縮的動態序列,slice 功能也更靈活。面試

數組的每一個元素能夠經過索引下標來訪問,索引下標的範圍是從 0 開始到數組長度減 1 的位置。內置的 len 函數將返回數組中元素的個數。數組

var a [3]int             // array of 3 integers
fmt.Println(a[0])        // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
複製代碼

默認狀況下,數組的每一個元素都被初始化爲元素類型對應的零值,對於數字類型來講就是 0。markdown

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 是兩種不一樣的數組類型。架構

數組的長度必須是常量表達式,由於數組的長度須要在編譯階段肯定。app

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[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 的語法和數組很像,只是沒有固定長度而已。post

一個 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,它們有重疊部分:

Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
複製代碼

append 函數

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

var runes []rune
for _, 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 所示。

在下一次迭代時 i=4,如今沒有新的空餘的空間了,所以 appendInt 函數分配一個容量爲 8 的底層數組,將 x 的 4 個元素[0 1 2 3]複製到新空間的開頭,而後添加新的元素 i,新元素的值是 4。新的 y 的長度是 5,容量是 8;後面有 3 個空閒的位置,三次迭代都不須要分配新的空間。當前迭代中,y 和 x 是對應不一樣底層數組的 view。此次操做如圖 4.3 所示。

內置的 append 函數可能使用比 appendInt 更復雜的內存擴展策略。

所以,一般咱們並不知道 append 調用是否致使了內存的從新分配,所以咱們也不能確認新的 slice 和原始的 slice 是否引用的是相同的底層數組空間。

一樣,咱們不能確認在原先的 slice 上的操做是否會影響到新的 slice。

做者:架構精進之路,十年研發風雨路,大廠架構師,CSDN 博客專家,專一架構技術沉澱學習及分享,職業與認知升級,堅持分享接地氣兒的乾貨文章,期待與你一塊兒成長。

關注「架構精進之路」公衆號並回復「01」,送你一份程序員成長進階大禮包,另外面試寶典、大量技術電子書免費領取。

Thanks for reading!

相關文章
相關標籤/搜索