golang的引用類型(slice,map,channel)底層實現


Slice

切片即動態數組,可以動態擴容改變數組的容量. golang 的 slice 底層結構如下所示,它是一個結構體,里面包含了指向數組的地址,並通過 len、cap 保存數組的元素數、容量:

type slice struct {
    array unsafe.Pointer // 指向數組的指針
    len   int // 切片中元素的數量
    cap   int // array 數組的總容量
}

切片拷貝:

考慮到切片 slice 的結構,對於切片直接用 = 拷貝,實際上是淺拷貝,只是改變了指針的指向,並沒有改變數組中元素的值. 對於深度拷貝的需求,可以借助 copy 內置函數完成. 兩種拷貝的方式如下:

  • 深度拷貝: copy(sliceA, sliceB)
  • 淺拷貝: sliceA = sliceB

切片之間的復制會拷貝數組指針、cap、len 值,但數組指針指向的是同一個地址. 如果想做深度拷貝,即將指針指向的數組內容而不是指針值進行拷貝. 可以使用內置的 copy 函數進行切片拷貝. 如下所示,使用 copy 進行復制,會改變 s2 地址的內存內的數組值.

var s1 = []int{1, 2}        // 初始化一個切片
var s2 = make([]int, 2)     // 初始化一個空的切片,cap為2
copy(s2, s1)                // 將s1拷貝給s2
s2[0] = 99                  // 改變s2[0]
fmt.Println(s1[0])          // 打印 1 而不是 99

切片 slice 函數傳遞

在切片進行復制時,會將切片的值(指針、cap、len)復制了一份. 在函數內部可以改變原切片的值.

但是,當涉及到 append 觸發擴容時,原來的指針指向的地址會發生變化,之后再對數組值進行更改,原切片將不受影響.

//定義一個函數,給切片添加一個元素
func addOne(s []int) {
    s[0] = 4  // 可以改變原切片值
    s = append(s, 1)  // 擴容后分配了新的地址,原切片將不再受影響
    s[0] = 8 
}
var s1 = []int{2}   // 初始化一個切片
addOne(s1)          // 調用函數添加一個切片
fmt.Println(s1)     // 輸出一個值 [4]

切片 slice 的擴容

當使用 append(slice,data) 時候,Golang 會檢查底層的數組的長度是否已經不夠,如果長度不夠,Golang 則會新建一個數組,把原數組的數據拷貝過去,再將 slice 中的指向數組的指針指向新的數組。

其中新數組的長度一般是老數組的倆倍,當然,如果一直是倆倍增加,那也會極大的浪費內存. 所以在老數組長度大於 1024 時候,將每次按照不小於 25% 的漲幅擴容.

slice 增加長度的源碼在 src/runtime/slice.go 的 growslice 函數中

Map

​map 字典是 golang 中高級類型之一,它提供鍵值對形式的存儲. 它也是引用類型,參數傳遞時其內部的指針被復制,指向的還是同一個內存地址. 當對賦值后的左值進行修改時,是會影響到原 map 值的.

map 的底層本質上是實現散列表,它解決碰撞的方式是拉鏈法. map 在進行擴容時不會立即替換原內存,而是慢慢的通過 GC 方式釋放.

hmap 結構

以下是 map 的底層結構,其源碼位於 src/runtime/map.go 中,結構體主要是 hmap .

// A header for a Go map.
type hmap struct {
  // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
  // Make sure this stays in sync with the compiler's definition.
  count     int // # live cells == size of map.  Must be first (used by len() builtin)
  flags     uint8
  B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
  noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
  hash0     uint32 // hash seed
​
  buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
  oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
  nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
​
  extra *mapextra // optional fields
}

上述代碼中 buckets、oldbuckets 是指向存儲鍵值的內存地址, 其中 oldbuckets 用於在擴容時候,指向舊的 bucket 地址,再下次訪問時不斷的將 oldbuckets 值轉移到 buckets 中. oldbuckets 並不直接釋放內存,而是通過不引用,交由 gc 釋放內存.

散列表和 bucket ( a bucket for a go map)

hmap 中核心的結構是 buckets,它是 bucket 數組,其中每個 bucket 是一個鏈表. 這個結構其實就是散列表的實現,通過拉鏈法消除 hash 沖突. 使得散列表能夠存儲更多的元素,同時避免過大的連續內存申請. 如下圖 1,是 golang buckets 數組在內存中的形式,buckets 數組的每個元素是鏈表的頭節點.

 

 

 在哈希表結構中有一個加載因子(即 loadFactor), 它一般是散列包含的元素數除以位置總數. 加載因子越高,沖突產生的概率越高. 當達到一定閾值時,就該為哈希表進行擴容了,否則查詢效率將會很低.

當 golang map 的加載因子大於閾值時,len(map) / 2 ^ B > 6.5 時 ,就會對 map 對象進行擴容. 擴容不會立刻釋放掉原來的 bucket 內存,而是由 oldbucket 指向,並產生新的 buckets 數組並由指針 buckets 指向. 在再次訪問原數據時,再依次將老的 bucket 移到新的 buckets 數組中. 同時解除對老的 bucket 的引用,GC 會統一釋放掉這些內存.

哈希函數

哈希函數是哈希表的特點之一,通過 key 值計算哈希,快速映射到數據的地址. golang 的 map 進行哈希計算后,將結果分為高位值和低位值,其中低位值用於定位 buckets 數組中的具體 bucket,而高位值用於定位這個 bucket 鏈表中具體的 key .

channel

hchan 結構

chan 源碼位於 src/runtime/chan.go 中,其結構體為 hchan,其中主要包括 buf、sendx、recvx、sendq、recvq 等.

type hchan struct {
  qcount   uint           // total data in the queue
  dataqsiz uint           // size of the circular queue
  buf      unsafe.Pointer // points to an array of dataqsiz elements
  elemsize uint16
  closed   uint32
  elemtype *_type // element type
  sendx    uint   // send index
  recvx    uint   // receive index
  recvq    waitq  // list of recv waiters
  sendq    waitq  // list of send waiters
// lock protects all fields in hchan, as well as several
  // fields in sudogs blocked on this channel.
  //
  // Do not change another G's status while holding this lock
  // (in particular, do not ready a G), as this can deadlock
  // with stack shrinking.
  lock mutex
}

主要結構的作用:

  • buf: 有緩沖通道用於存儲緩存數據的空間, 它是一個循環鏈表.
  • sendx 和 recvx: 用於記錄循環鏈表 buf 中的發送或者接受的 index.
  • sendq 和 recvq: 是倆個雙向隊列,分別是發送、接受的 goroutine 抽象出來的 sudog 結構體的隊列.
  • lock: 互斥鎖. 在 send 和 recv 操作時鎖住 hchan.

make 創建通道

使用 make 可以創建通道,如下示例:

ch0 := make(chan int)   // 無緩沖通道
ch1 := make(chan int, 3)   // 有緩沖通道

創建通道實際上是創建了一個 hchan 結構體,返回指針 ch0 . chan 在 go 語言中是引用類型, 在參數傳遞過程是復制的是這個指針值.

send 和 recv

首先會使用 lock 鎖住 hchan. 然后以 sendx 或 recvx 序號,在循環鏈表 buf 指定的位置上找到數據,將數據 copy 到 goroutine 或者時從 goroutine copy 的 buf 上. 然后釋放鎖.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM