1. 程式人生 > 其它 >Golang slice、array 原始碼

Golang slice、array 原始碼

前言​

  在golang中有很多的資料結構是很常用的資料結構,比如array,slice,map等,其中最為常用的就是array和slice還有map了,理論上來講array和slice在資料結構上是一種結構,都是順序結構,但是由於array的固定長度特性,在有些時候對於需要動態的長度的使用情況很不友好,此時就需要利用slice進行對固定長度陣列的代替。

什麼是Slice

官方解釋如下:

​   Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for
items with explicit dimension such as transformation matrices, most array programming in Go is done with slices rather than simple arrays. 大概意思如下:   Slice是一個經過包裝的array,其可為資料序列提供更通用,更強大和更方便的介面。 除了具有明確維數的項(例如轉換矩陣)外,Go中的大多數陣列程式設計都是使用切片而不是簡單陣列完成的。

  切片個人認為有點像c++標準庫中的vector,只不過是底層的實現方式可能有些許不同(不太瞭解c++,如果有大佬知道vector的底層實現的話,可以解惑一下),slice是一個把go陣列進行了包裝的一個結構體,但是這個結構體只是在編譯等其他層面能看到,在我們使用過程中只需要像定義陣列那樣定義就可以在編譯期間被轉換為slice結構體。接下來我來解析一下slice的相關結構體原始碼以及操作原始碼。

程式碼解析

slice的結構體

 1 // slice 結構體,這個結構體會在編譯期間構建
 2 // 如果想在執行期間使用的話可以使用其對應的reflect結構體
 3 // 即reflect.SliceHeader
 4 type slice struct {
 5     // 一個指向底層陣列的指標
 6     array unsafe.Pointer
 7     // slice當前元素個數 即len()時返回的數
 8     len   int
 9     // slice的容量 即cap()時返回的數
10     cap   int
11 }

slice的初始化

slice的初始化方式分為三種:

  1. 下標初始化方式:a:=slice[:] // 這個slice是一個其他以建立好的slice。這種建立方式時最接近底層的建立方式,在編譯期間該語句會被轉換為編譯器的OpSliceMake操作,該操作會呼叫SliceMake操作,SliceMake操作會接受四個引數建立新的切片,元素型別、陣列指標、切片大小和容量,這與上一章所展示的slice的結構體的欄位構成相同,另外下標初始化方式不會對原陣列中的資料進行復制,而是直接引用指向原陣列的指標,這會導致在修改切片a時,對原切片slice也會產生影響。
  2. 字面量初始化方式:```a:=[]int{1,2,3}``,過程如下
  • 該方法在編譯期間建立首先會生成一個長度為3(該長度根據字面量數量自動推斷)的陣列
  • 給這個陣列的元素進行賦值
  • 而後會new出一個新的長度為3(該長度根據字面量數量
  • 自動推斷)的陣列的指標
  • 將這個指標按照最基本的下標初始化方式進行賦值給slice
1 var arr [3]int
2 arr[0] = 1
3 arr[1] = 2
4 arr[2] = 3
5 var arrp *[3]int = new([3]int)
6 *arrp = arr
7 slice := arrp[:]

  3.通過關鍵字make([]int,3,3)建立切片:通過make關鍵字進行切片初始化,首先會在編譯階段對其傳入的len與cap進行校驗,校驗其是否為負值,是否len>cap,並且通過判斷切片的大小以及是否逃逸來確定其是否會初始化在堆上,如果當切片足夠小並且沒有發生逃逸時,會建立一個cap值的陣列,然後像字面量初始化方式一樣對其進行初始化,如果cap為0,則按照len的值建立對應長度的陣列。如果發生逃逸或者切片過大時,會呼叫runtime.makeslice()函式進行堆上的切片記憶體分配。runtime.makeslice程式碼如下:

 1 // 該函式傳入需要初始化的切片的型別,長度以及容量,返回的指標會通過呼叫方組建成一個完成的slice結構體
 2 func makeslice(et *_type, len, cap int) unsafe.Pointer {
 3   // 判斷型別,和容量的乘積會不會超過可分配記憶體的大小,以及長度是否為0和容量是否小於長度
 4     mem, overflow := math.MulUintptr(et.size, uintptr(cap))
 5     if overflow || mem > maxAlloc || len < 0 || len > cap {
 6         mem, overflow := math.MulUintptr(et.size, uintptr(len))
 7         if overflow || mem > maxAlloc || len < 0 {
 8             panicmakeslicelen()
 9         }
10         panicmakeslicecap()
11     }
12     // 如果都正常,則呼叫此函式申請返回一個連續 切片中元素大小×切片容量 長度的記憶體空間的指標
13     return mallocgc(mem, et, true)
14 }

訪問元素

  slice的訪問元素是通過slice結構體中

對應索引的元素地址=指向底層陣列的指標+對應元素的佔用位元組數∗索引

  編譯器會通過對應索引的元素地址返回其中對應的值,即直接進行地址訪問

追加和擴容

​  slice相比於array的一大優點就是可以根據使用情況動態的進行擴容,來適應隨時增加的資料,在追加時,通過呼叫append函式來針對slice進行尾部追加,如果此時slice的cap值小於當前len加上append中傳入值的數量,那麼就會出發擴容操作,append函式沒有明確的函式體,而是通過編譯期間被轉換。當append發現需要擴容時,則會呼叫runtime.growslice方法,該方法原始碼如下(以去除一些無用程式碼):

 1 func growslice(et *_type, old slice, cap int) slice {
 2     // 如果需求的容量小於就容量則報錯
 3   // 理論上來講不應該出現這個問題
 4     if cap < old.cap {
 5         panic(errorString("growslice: cap out of range"))
 6     }
 7     // append 沒法建立一個nil指標的但是len不為0的切片
 8     if et.size == 0 {
 9         return slice{unsafe.Pointer(&zerobase), old.len, cap}
10     }
11     
12     newcap := old.cap
13     doublecap := newcap + newcap
14   // 如果需求容量大於雙倍的舊容量那就直接使用需求容量
15     if cap > doublecap {
16         newcap = cap
17     } else {
18     // 如果當前len小於1024則容量直接翻倍,否則按照1.25倍去遞增直到滿足需求容量
19         if old.len < 1024 {
20             newcap = doublecap
21         } else {
22             for 0 < newcap && newcap < cap {
23                 newcap += newcap / 4
24             }
25             if newcap <= 0 {
26                 newcap = cap
27             }
28         }
29     }
30 
31     var overflow bool
32     var lenmem, newlenmem, capmem uintptr
33 // 在擴容時不能單單按照len來判斷擴容所需要的記憶體長度
34 // 還要根據切片的元素型別去進行記憶體對齊
35 // 當元素的佔用位元組數為1,8 或者2的倍數時會進行記憶體對對齊
36 // 記憶體對齊策略按照向上取整方式進行
37 // 取整的目標時go記憶體分配策略中67個class分頁中的大小進行取整
38     switch {
39     case et.size == 1:
40         lenmem = uintptr(old.len)
41         newlenmem = uintptr(cap)
42         capmem = roundupsize(uintptr(newcap))
43         overflow = uintptr(newcap) > maxAlloc
44         newcap = int(capmem)
45     case et.size == sys.PtrSize:
46         lenmem = uintptr(old.len) * sys.PtrSize
47         newlenmem = uintptr(cap) * sys.PtrSize
48         capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
49         overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
50         newcap = int(capmem / sys.PtrSize)
51     case isPowerOfTwo(et.size):
52         var shift uintptr
53         if sys.PtrSize == 8 {
54             // Mask shift for better code generation.
55             shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
56         } else {
57             shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
58         }
59         lenmem = uintptr(old.len) << shift
60         newlenmem = uintptr(cap) << shift
61         capmem = roundupsize(uintptr(newcap) << shift)
62         overflow = uintptr(newcap) > (maxAlloc >> shift)
63         newcap = int(capmem >> shift)
64     default:
65         lenmem = uintptr(old.len) * et.size
66         newlenmem = uintptr(cap) * et.size
67         capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
68         capmem = roundupsize(capmem)
69         newcap = int(capmem / et.size)
70     }
71 
72 // 如果所需要的記憶體超過了最大可分配記憶體則panic
73     if overflow || capmem > maxAlloc {
74         panic(errorString("growslice: cap out of range"))
75     }
76 
77     var p unsafe.Pointer
78   // 如果當前元素型別不是指標,則會將超出切片當前長度的位置清空
79   // 並在最後使用 將原陣列記憶體中的內容拷貝到新申請的記憶體中。
80     if et.ptrdata == 0 {
81         p = mallocgc(capmem, nil, false)
82         memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
83     } else {
84     // 如果是指標會根據進行gc方面對其進行加以保護以免空指標在分配期間被gc回收
85         p = mallocgc(capmem, et, true)
86         if lenmem > 0 && writeBarrier.enabled {
87             bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
88         }
89     }
90     memmove(p, old.array, lenmem)
91     //該函式最終會返回一個新的切片
92     return slice{p, old.len, newcap}
93 }

slice的拷貝

  slice的拷貝也是針對切片提供的介面,可以通過呼叫copy()函式將src切片中的值拷貝到dst切片中,通過該函式進行的切片拷貝後,針對dst切片進行的操作不會對src產生任何的影響,其拷貝長度是按照src與dst切片中最小的len長度去計算的,runtime.slicecopy原始碼如下:

func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int {
    if fmLen == 0 || toLen == 0 {
        return 0
    }

    n := fmLen
    if toLen < n {
        n = toLen
    }

    if width == 0 {
        return n
    }
    
    size := uintptr(n) * width
    if size == 1 {  
    // 如果就1個元素 直接賦值過去就好了
        *(*byte)(toPtr) = *(*byte)(fmPtr)
    } else {
    // 直接進行記憶體的拷貝,如果slice資料量過大將會影響效能
        memmove(toPtr, fmPtr, size)
    }
    return n
}
you are the best!