Go Slice底層實現

1 Go數組

Go數組是值類型,數組定義的時候就須要指定大小,不一樣大小的數組是不一樣的類型,數組大小固定以後不可改變。數組的賦值和傳參都會複製一份。數組

func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int

    arrayB = arrayA

    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)

    testArray(arrayA)
}

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}
複製代碼

結果:markdown

arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]
複製代碼

能夠看到,三個內存地址都不一樣,這也就驗證了 Go 中數組賦值和函數傳參都是值複製的。尤爲是傳參的時候把數組複製一遍,當數組很是大的時候會很是消耗內存。能夠考慮使用指針傳遞。數據結構

指針傳遞有個很差的地方,當函數內部改變了數組的內容,則原數組的內容也改變了。app

所以通常參數傳遞的時候使用slice函數

2 切片的數據結構

切片自己並非動態數組或者數組指針。它內部實現的數據結構經過指針引用底層數組,設定相關屬性將數據讀寫操做限定在指定的區域內。切片自己是一個只讀對象,其工做機制相似數組指針的一種封裝。
oop

切片(slice)是對數組一個連續片斷的引用,因此切片是一個引用類型。這個片斷能夠是整個數組,或者是由起始和終止索引標識的一些項的子集。須要注意的是,終止索引標識的項不包括在切片內。切片提供了一個與指向數組的動態窗口。
ui

給定項的切片索引可能比相關數組的相同元素的索引小。和數組不一樣的是,切片的長度能夠在運行時修改,最小爲 0 最大爲相關數組的長度:切片是一個長度可變的數組。
spa

切片數據結構定義3d

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
複製代碼

image.png
切片的結構體由3部分構成,Pointer 是指向一個數組的指針,len 表明當前切片的長度,cap 是當前切片的容量。cap 老是大於等於 len 的。
image.png
若是想從 slice 中獲得一塊內存地址,能夠這樣作:指針

s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
複製代碼

3 建立切片

3.1 方法一:make

使用make函數建立slice

// 建立一個初始大小是3,容量是10的切片
s1 := make([]int64,3,10)
複製代碼

底層方法實現:

func makeslice(et *_type, len, cap int) slice {
    // 根據切片的數據類型,獲取切片的最大容量
    maxElements := maxSliceCap(et.size)
    // 比較切片的長度,長度值域應該在[0,maxElements]之間
    if len < 0 || uintptr(len) > maxElements {
        panic(errorString("makeslice: len out of range"))
    }
    // 比較切片的容量,容量值域應該在[len,maxElements]之間
    if cap < len || uintptr(cap) > maxElements {
        panic(errorString("makeslice: cap out of range"))
    }
    // 根據切片的容量申請內存
    p := mallocgc(et.size*uintptr(cap), et, true)
    // 返回申請好內存的切片的首地址
    return slice{p, len, cap}
}
複製代碼

3.2 方法二:字面量

利用數組建立切片

arr := [10]int64{1,2,3,4,5,6,7,8,9,10}
s1 := arr[2:4:6] // 以arr[2:4]建立一個切片,且容量到達arr[6]的位置,即cap=6-2=4,若是不寫容量則默認爲數組最後一個元素
複製代碼

4 nil和空切片

image.png
nil切片的指針指向的是nil

image.png
空切片指向的是一個空數組

空切片和 nil 切片的區別在於,空切片指向的地址不是nil,指向的是一個內存地址,可是它沒有分配任何內存空間,即底層元素包含0個元素。

最後須要說明的一點是。不論是使用 nil 切片仍是空切片,對其調用內置函數 append,len 和 cap 的效果都是同樣的

5 切片擴容

5.1 擴容策略

func main() {
    slice := []int{10, 20, 30, 40}
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}
複製代碼

輸出結果:

Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
複製代碼

Go 中切片擴容的策略是這樣的:

若是切片的容量小於 1024 個元素,因而擴容的時候就翻倍增長容量。上面那個例子也驗證了這一狀況,總容量從原來的4個翻倍到如今的8個。

一旦元素個數超過 1024 個元素,那麼增加因子就變成 1.25 ,即每次增長原來容量的四分之一。

5.2 底層數組是否是新地址

不必定。當發生了擴容就確定是新數組,沒有發生擴容則是舊地址

無論切片是經過make建立仍是字面量建立,底層都是同樣的,指向的是一個數組。當使用字面量建立時,切片底層使用的數組就是建立時候的數組。修改切片中的元素或者往切片中添加元素,若是沒有擴容,則會影響原數組的內容,切片底層和原數組是同一個數組;當切片擴容了以後,則修改切片的元素或者往切片中添加元素,不會修改數組內容,由於切片擴容以後,底層數組再也不是原數組,而是一個新數組。

因此儘可能避免切片底層數組與原始數組相同,儘可能使用make建立切片

range遍歷數組或者切片須要注意

func main() {
	// slice := []int{10, 20, 30, 40}
	slice := [4]int{10, 20, 30, 40}
	for index, value := range slice {
		fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
	}
}
複製代碼

結果:

value = 10 , value-addr = c00000a0a8 , slice-addr = c000012360
value = 20 , value-addr = c00000a0a8 , slice-addr = c000012368
value = 30 , value-addr = c00000a0a8 , slice-addr = c000012370
value = 40 , value-addr = c00000a0a8 , slice-addr = c000012378
複製代碼

從上面結果咱們能夠看到,若是用 range 的方式去遍歷一個數組或者切片,拿到的 Value 實際上是切片裏面的值拷貝。因此每次打印 Value 的地址都不變。
image.png
因爲 Value 是值拷貝的,並不是引用傳遞,因此直接改 Value 是達不到更改原切片值的目的的,須要經過 &slice[index] 獲取真實的地址

尤爲是在for循環中使用協程,必定不能直接把index,value傳入協程,而應該經過參數傳進去
錯誤示例:

func main() {
	s := []int{10,20,30}
	for index, value := range s {
		go func() {
			time.Sleep(time.Second)
			fmt.Println(fmt.Sprintf("index:%d,value:%d", index,value))
		}()
	}
	time.Sleep(time.Second*2)
}

結果:
index:2,value:30
index:2,value:30
index:2,value:30
複製代碼

正確示例:

func main() {
	s := []int{10,20,30}
	for index, value := range s {
		go func(i,v int) {
			time.Sleep(time.Second)
			fmt.Println(fmt.Sprintf("index:%d,value:%d", i,v))
		}(index,value)
	}
	time.Sleep(time.Second*2)
}
結果:
index:0,value:10
index:2,value:30
index:1,value:20
複製代碼

參考

【1】深刻解析 Go 中 Slice 底層實現

相關文章
相關標籤/搜索