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 上. 然后釋放鎖.