Go語言實戰筆記 數組、切片和映射

封面

Go數組

內部實現

在Go語言裏面,數組是長度固定的數據類型,必須存儲一段相同類型的元素,而且這些元素是連續的。我們這裏強調固定長度,可以說是和切片最明顯的區別。

數組存儲的類型可以是內置類型,比如整型或者字符串,也可以是自定義的數據結構。因爲是連續的,所以索引比較好計算,所以我們可以很快的索引數組中的任何數據。

這裏的索引,一直都是0,1,2,3這樣的,因爲其元素類型相同,我們也可以使用反射,獲取類型佔用大小,進行移位,獲取相對應的元素。

聲明和初始化

var array [5]int

這裏聲明瞭一個數組array,但是並沒有進行初始化賦值,這時數組array裏面的值,默認賦值爲整型的零值。

數組一旦聲明後,數組裏存儲的數據類型和數組長度就都不能改變了。如果需要存儲更多的元素,就需要創建另一個更長的數組,再把原來數組的值複製到新數組裏面。

對已被默認初始化爲零值的數組,再次初始化

var array [5]int
array = [5]int{10, 20, 30, 40, 50}

讓Go自動計算聲明數組的長度

array := [...]int{10, 20, 30, 40, 50}

使用:=操作符聲明數組時並指定特定元素的值

array := [5]int{10, 20, 30, 40, 50}

讓特定索引值爲零

array := [5]int{0,1,3,0,0} // 0,3,4索引對應的值爲0
array:=[5]int{1:1,3:4} // 只初始化索引1和3的值

使用數組

因爲數組內存分佈是連續的,所以數組是效率很高的數據結構。可以通過使用[]操作符訪問數組裏某個單獨元素。

array := [5]int{1, 2, 3, 4, 5}
array[2] = 20
fmt.Printf(array[2])

訪問指針數組元素。

array := [5]*int{0:new(), 1:new()} // 聲明包含5個元素的指向整型的數組,並用整型指針初始化索引爲0,1的數組元素

// 爲索引0,1的元素賦值
*array[0] = 10
*array[1] = 20

把同類型的一個數組賦值給另外一個數組

var newarray [5]string
array := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
newarray = array

把指針數組賦值給另一個

var newarray [3]*string
array := [3]*string{new(string), new(string), new(string)}

*array[0] = "Red"
*array[1] = "Blue"
*array[2] = "Green"

newarray = array

多維數組的聲明和初始化

聲明二維數組

var array [4][2]int

array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}

array := [4][2]int{1: {10, 11}, 3: {20, 21}} // 聲明並初始化外層數組中索引1,3元素

array := [4][2]int{1: {0: 20}, 3: {1: 22}} // 聲明並初始化外層和內層數組的元素

在函數間傳遞數組

在函數間傳遞變量時,總是以值的方式傳遞的。如果這個變量是一個數組,意味着整個數組,不管有多長,都會完整複製,並傳遞給函數,這會是一個開銷很大的操作。

func main() {
    var array [1e6]int // 聲明一個8MB的數組
    transfer(array)
    fmt.Println(array)
}

func transfer(a [1e6]int) {
    a[0] = 99999
    fmt.Println(a)
}

通過輸出a結果可以看到數組是複製的,原數組並沒有發生改變。

爲了減少複製帶來的開銷,可以使用指針在函數間傳遞大數組

func main() {
    var array [1e6]int // 聲明一個8MB的數組
    transfer(&array)
    fmt.Println(array)
}

func transfer(a ×[1e6]int) {
    a[0] = 99999
    fmt.Println(a)
}

因爲現在傳遞的是指針,所以改變指針指向的值,會改變共享的內存。

這裏注意,數組的指針和指針數組是兩個概念,數組的指針是[5]int,指針數組是[5]int,注意*的位置。


Go切片

內部實現

切片是一種數據結構,這種數據結構便於使用和管理數據集合。切片是圍繞動態數組的概念構建的,可以按需自動增長和縮小,其底層內存也是在連續塊中分配的,效率非常高,可以通過對切片再次切片來縮小一個切片的大小,還可以通過索引獲得數據、迭代以及垃圾回收優化的好處。

切片是一個很小的對象,對底層數組進行了抽象,切片是隻有三個字段的數據結構:指向底層數組的指針、切片訪問的元素個數(即長度)和切片允許增長到的元素個數(即容量)。

聲明和初始化

使用長度聲明一個字符串切片

slice := make([]string, 5) // 創建一個字符切片,長度和容量都是5

使用長度和容量聲明整形切片,分別指定長度和容量時,聲明切片,底層數組的長度是指定的容量,但是初始化後並不能訪問所有的元素。

slice := make([]string, 3, 5) // 其長度爲3個元素,容量爲5個元素

容量必須 >= 長度,不能創建長度 > 容量的切片

通過切片字面量來聲明切片

slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}

使用索引聲明切片

slice := []string{99: ""} // 使用空字符串初始化第100個元素

切片聲明時不需要在[]操作符裏聲明長度

nil和空切片

在聲明時不做任何初始化,就會創建一個nil切片

var slice []int

聲明空切片

slice := make([]int, 0) // 使用make創建空的整形切片
slice := []int{} // 使用字面量創建空的整型切片

nil切片和空切片,它們的長度和容量都爲0,但它們指向的底層數組的指針不一樣,nil切片意味着指向底層數組的指針爲nil,而空切片對應的指針是地址。

使用切片

使用切片字面量來聲明切片

slice := []int{10, 20, 30, 40, 50}
slice[1] = 25

使用切片創建切片

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]

這兩段切片共享同一段底層數組,所以當修改的時候,底層數組的值就會發生改變,同時原切片的值也改變了。

如何計算新的切片的長度和容量

對底層數組容量爲k的切片slice[i:j]來說

長度: j - i

容量: k - i

使用3個索引創建切片

source := []string{"Apple", "orange", "Plum", "Banana", "Grape"}
slice := source[2:3:4]

這樣創建了長度爲1,容量爲2的切片

如何計算3個索引創建的切片的長度和容量

對底層數組容量爲k的切片slice[i:j:k]來說

長度: j - i

容量: k - i

切片增長

使用append向切片增加元素。

slice := []int{10, 20, 30, 40, 50}

newSlice := slice[1:3]

newSlice = append(newSlice, 60)

使用append同時增加切片的長度和容量

slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)

append函數會智能地處理底層數組的容量增長,在切片的容量小於1000個元素時,總是成倍地增長容量,一旦切片的容量超過1000個,容量的增長因子會設爲1.25,也就是每次增長25%的容量。

將一個切片追加到另一個切片

s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1, s2))

Output:
[1 2 3 4]

迭代切片

使用for range迭代切片

range創建了每個元素的副本,而不是直接返回對該元素的引用。

slice := []int{10, 20, 30, 40}

for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

如果不需要索引值,可以使用佔位字符(下劃線_)來忽略索引值

slice := []int{10, 20, 30, 40}

for _, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

使用傳統的for循環對切片進行迭代

slice := []int{10, 20, 30, 40}

for index := 2, index < len(slice) {
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

多維切片

聲明多維切片

slice := [][]int{{10}, {100, 200}}

slice[0] = append(slice[0], 20) // 組合切片的切片

在函數間傳遞切片

在函數間傳遞切片就是要在函數間以值的方式傳遞切片。由於切片的尺寸很小,在函數間複製和傳遞切片成本也很低。

func main() {
    slice := make([]int, 1e6) // 分配包含100萬個整型值的切片
     fmt.Printf("%p\n",&s)
    transfer(slice)
    fmt.Println(slice)
}

func transfer(s []int) {
    fmt.Printf("%p\n",&s)
    s[0] = 99999
    fmt.Println(s)
}

0x40c0e0

0x40c0f0

輸出的兩個切片對應地址不一樣,可以確認切片在函數間傳遞是複製的。而我們修改一個索引的值後,原切片的值也被跟着修改了,說明它們共用一個底層數組。

Go映射

內部實現

映射是一種數據結構,用於存儲一系列無序的鍵值對。映射基於鍵來存儲值,鍵就像索引一樣,可以快速檢索數據,鍵指向與該鍵關聯的值。

映射是一個集合,使用了散列表來實現,可以使用類似處理數組和切片的方式迭代映射中的元素。但映射是無序的集合,意味着沒有辦法預測鍵值對被返回的順序。

映射的散列表包含一組桶,每次存儲和查找鍵值對的時候,都要先選擇一個桶。把操作映射時指定的鍵傳給映射的散列函數,就能選中對應的桶。這個散列函數的目的是生成一個索引,這個索引最終將鍵值對分佈到所有可用的桶裏。

這種方式的好處在於,存儲的數據越多,索引分佈越均勻,所以我們訪問鍵值對的速度也就越快。

聲明和初始化

使用make聲明映射

dict := make(map[string]int) // 創建空映射
dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"} // 給空映射賦值

使用映射字面量聲明空映射

dict := map[string]int{}

通過聲明映射創建一個nil映射

var dict map[string]int // 聲明nil映射

dict = make(map[string]int) // 給nil映射分配內存空間
dict["A"] = 1 // 給nil映射初始化賦值

使用映射

從映射獲取值並判斷鍵是否存在

values, exists := colors["Blue"]
if exists {
    fmt.Println(value)
}

從映射中刪除一項

delete(dict, "Blue")

使用range迭代映射

dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"} 
for key, value := range dict {
    fmt.Println(key, value)
}

在函數間傳遞映射

在函數間傳遞並不會製造出該映射的一個副本,當傳遞映射給一個函數,並對這個映射做了修改,所有對這個映射的引用都會察覺到這個修改。

func main() {
    dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"}
    transfer(dict)
    fmt.Println(dict)
}

func transfer(d map[string]int) {
    s["Red"] = "#fffff"
    fmt.Println(dict)
}

部分引用了飛雪無情大佬的博客 http://www.flysnow.org/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章