天天看點

Golang 語言中數組和切片的差別是什麼?

01

介紹

在很多程式設計語言中都有數組,而切片類型卻不常見。實際上,Golang 語言中的切片的底層存儲也是基于數組。因為數組是固定長度的,而切片比數組更加靈活,是以在 Golang 語言中,數組使用的并不多,切片使用更加廣泛。

02

數組和切片的差別

  • 數組的零值是元素類型的零值,切片的零值是 nil;
  • 數組是固定長度,切片是可變長度;
  • 數組是值類型,切片是引用類型。

數組:

func main () {
    var arr1 [4]int
    fmt.Printf("arr1 val:%d arr1 len:%d arr1 cap:%d\n", arr1, len(arr1), cap(arr1))
    arr := [4]int{}
    fmt.Printf("val:%d len:%d cap:%d\n", arr, len(arr), cap(arr)) // val:[0 0 0 0] len:4 cap:4
    arr[0] = 1
    arr[1] = 2
    arr[2] = 3
    arr[3] = 4
    // arr[4] = 5 // invalid array index 4 (out of bounds for 4-element array)
    fmt.Printf("val:%d len:%d cap:%d\n", arr, len(arr), cap(arr)) // val:[1 2 3 4] len:4 cap:4
    arr2 := arr
 fmt.Printf("arr2 val:%d len:%d cap:%d ptr:%p\n", arr2, len(arr2), cap(arr2), &arr2) // arr2 val:[1 2 3 4] len:4 cap:4 ptr:0xc0001980a0
 fmt.Printf("arr val:%d len:%d cap:%d ptr:%p\n", arr, len(arr), cap(arr), &arr) // arr val:[1 2 3 4] len:4 cap:4 ptr:0xc000198040
 ss := arr[:]
 ssPtr := (*reflect.SliceHeader)(unsafe.Pointer(&ss)).Data
 fmt.Printf("ss val:%d len:%d cap:%d ptr:%v\n", ss, len(ss), cap(ss), ssPtr) // ss val:[1 2 3 4] len:4 cap:4 ptr:824635392064
 ss2 := arr[:]
 ss2Ptr := (*reflect.SliceHeader)(unsafe.Pointer(&ss2)).Data
 fmt.Printf("ss2 val:%d len:%d cap:%d ptr:%v\n", ss2, len(ss2), cap(ss2), ss2Ptr) // ss2 val:[1 2 3 4] len:4 cap:4 ptr:824635392064
}
           

複制

切片:

func main () {
  var s []int
 if s == nil {
  fmt.Println("nil")
 }
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[] len:0 cap:0
 s = append(s, 1)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1] len:1 cap:1
 s = append(s, 2)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1 2] len:2 cap:2
 s = append(s, 3)
 fmt.Printf("s val:%d len:%d cap:%d\n", s, len(s), cap(s)) // s val:[1 2 3] len:3 cap:4
}
           

複制

閱讀上面這兩段代碼,我們可以發現數組的零值是元素類型的零值,而切片的零值是 nil,同時,nil 也是唯一可以和切片類型作比較的值。

數組中元素超越邊界會引發錯誤,切片中元素超越邊界會自動擴容,切片的擴容規則将在 Part 03 介紹。

數組是值類型,切片是引用類型。arr2 和 arr 的記憶體位址不同,它們是兩塊不同的記憶體空間;ss 和 ss2 的記憶體位址相同,它們指向同一個底層數組。

在 Golang 語言中傳遞數組屬于值拷貝,如果數組的元素個數比較多或者元素類型的大小比較大時,直接将數組作為函數參數會造成性能損耗,可能會有讀者想到使用數組指針作為函數參數,這樣是可以避免性能損耗,但是在 Golang 語言中,更流行使用切片,關于這塊内容,閱讀完 Part 04 的切片資料結構,會有更加深入的了解。

03

切片擴容規則

通過閱讀 Part 02 關于切片的這段代碼,我們還可以看出切片的擴容規則,當一個切片的容量無法存儲更多元素時,切片會自動擴容,它會生成一個容量更大的新切片,然後把原切片的元素和新元素一起拷貝到新切片中。

在原切片長度小于 1024 時,新切片的容量會按照原切片的 2 倍擴容,否則,新切片的容量會按照原切片的 1.25 倍擴容,此時需要注意的是,如果新切片的容量按照原切片的 1.25 倍擴容一次仍然無法存儲新元素時,将會不斷按照原切片的 1.25 倍擴容,直到新切片的容量可以存儲原切片的元素和新元素為止。一般最終擴容後的新切片,它的容量會大于或等于原切片的容量。

需要注意的是,當切片的零值是 nil 時,切片此時還沒有指向底層數組。但是切片的零值是可用的,當使用 append 向零值切片追加元素時,将會先給切片配置設定一個底層數組。

切片擴容實際是建立一個新的底層數組,把原切片的元素和新元素一起拷貝到新切片的底層數組中,原切片的底層數組将會被垃圾回收。

注意:切片的容量可以根據元素的個數的增多自動擴容,但是不會根據元素的個數的減少自動縮容。

04

切片資料結構

在 Golang 語言中,切片實際是一個結構體,源碼如下所示:

// /usr/local/go/src/runtime/slice.go
type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}
           

複制

閱讀源碼,我們可以發現先,slice 結構體包含 3 個字段:

  • array - 指向底層數組
  • len - 切片的長度
  • cap - 切片的容量

在 Golang 語言運作時中,一個切片類型的變量實際上就是

runtime.slice

結構體的執行個體,其中 arrray 字段是指針類型,指向切片的底層數組,len 是切片的長度,cap 是切片的容量,當使用 make 函數建立切片時,如果不指定 cap 參數的值,cap 的值就等于 len 的值。

05

切片程式設計技巧

如果已經認真閱讀完以上内容,我們應該已經知道切片在每次擴容時都會将原切片底層數組的元素和新元素一起拷貝到新切片的底層數組,這種操作在元素比較多或者元素的類型大小比較大時,記憶體配置設定和拷貝的代價還是比較大的。

為了降低或避免記憶體配置設定和拷貝的代價,我們通常會為新建立的切片指定 cap 參數的值,比如:

s := make([]T, 0, cap)
           

複制

但是,這種使用方式的前提是,我們可以預估切片的元素個數。

06

for range 周遊切片

通過使用

for range

周遊切片,每次周遊操作實際上是對周遊元素的拷貝。而使用 for 周遊切片,每次周遊是通過索引通路切片元素,性能會遠高于通過

for range

周遊。

是以想要優化使用

for range

周遊切片的性能,可以使用空白辨別符

_

省略每次周遊傳回的切片元素,改為使用切片索引取通路切片的元素。

普通方式:

func main () {
    s := make([]int, 0, 10000)
    for k, v := range s {
        fmt.Println(s, v)
    }
}
           

複制

優化方式:

func main () {
    s := make([]int, 0, 10000)
    for k, _ := range s {
        fmt.Println(k, s[k])
    }
}
           

複制

07

總結

本文我們先是介紹了數組和切片的差別,然後還介紹了一些關于切片的擴容規則、資料結構和使用技巧等。文中代碼比較多,建議讀者将代碼拷貝到編輯器中,檢視運作結果,進而可以更加深刻了解文中的内容。如果想了解更多數組和切片的内容,請閱讀推薦閱讀清單中的相關文章。