歡迎訪問我的部落格網站,可聊天,可寫文章https://www.weiboke.online
簡單用例
func main() {
a := make(map[int]int)
a[2] = 1
fmt.Println(a[2])
}
複製程式碼
golang 使用map是相當的簡單,直接使用,無需調包。出於興趣也是出於疑問,想深入瞭解map,以及map為啥不是併發安全。上面的用例很簡單,使用map一定要make一下,才能使用,否則會因為map為nil而panic。
map的容器
map的容器結構是由bmap結構陣列組成,只有理解了bmap的結構,才能理解map如何CURD操作
如何make一個map
對於上面的例子裡,要make一個map會呼叫makemap_small函式,初始化一個hmap的結構
func makemap_small() *hmap {
h := new(hmap) //new 一個hmap
h.hash0 = fastrand() //計算hash種子,用於hash時的引數
return h
}
// 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
}
複製程式碼
- count : map中key的個數
- buckets:一個指標,指向bmap陣列的頭指標
- B :buckets數量的log2
如何map[k]=v
型別的不同,可能會呼叫不同的函式,不過原理是類似的。對於上面的用例,會呼叫mapassign_fast64函式
func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
...
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.key.alg.hash(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
// Set hashWriting after calling alg.hash for consistency with mapassign.
h.flags |= hashWriting
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
bucket := hash & bucketMask(h.B)
...
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
var insertb *bmap
var inserti uintptr
var insertk unsafe.Pointer
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] == empty {
if insertb == nil {
insertb = b
inserti = i
}
continue
}
k := *((*uint64)(add(unsafe.Pointer(b), dataOffset+i*8)))
if k != key {
continue
}
insertb = b
inserti = i
goto done
}
...
}
...
insertb.tophash[inserti&(bucketCnt-1)] = tophash(hash) // mask inserti to avoid bounds checks
insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*8)
// store new key at insert position
*(*uint64)(insertk) = key
h.count++
done:
val := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*8+inserti*uintptr(t.valuesize))
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
return val
}
複製程式碼
從上面的程式碼可以看到,如果map沒被初始化會panic,map會做簡單的併發檢查,即檢查寫標誌位(h.flags&hashWriting)是否被設定為1,當為1是則panic,但這種檢查並不有效,尤其在多核處理器上,所以map不是併發安全。接著計算出key的hash值,並把寫標誌位置為1。buckets是map的容器,用來裝值。如果buckets為空,初始化一個物件。bucketMask計算掩碼值(如果h.B=4,則bucketMask(h.B)=1111(2);15(10)),通過hash和掩碼值相與得到是第幾個bucket。根據bucket值,計算偏移獲取bmap指標值。開始遍歷查詢是否有空的位置,存放這個key,或者是否有相同的key。如果bmap裡面沒有相同的key並且也有位置存放這個key,然後將tophash[inserti&(bucketCnt-1)]賦值為hash值的第一個位元組,主要用來標誌已存放有值。計算存放key的位置指標,賦值為key,將hmap的count加一。最後計算存放val的位置指標,將寫標誌位置零,將指標返回,編譯器會自動新增指令,將使用者程式碼中的value存到這個指標中。如果key已存在,就會直接跳到done區,也就是計算val的位置指標,將寫標誌位置零,將指標返回。
跳過了map增長之後的程式碼細節,可以後面繼續深入。
如何獲取map[k]的value值
弄明白上面如何存放value的原理,取也就是相當的簡單了。
func mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
...
if h == nil || h.count == 0 {
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
var b *bmap
if h.B == 0 {
// One-bucket table. No need to hash.
b = (*bmap)(h.buckets)
} else {
hash := t.key.alg.hash(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
m := bucketMask(h.B)
b = (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
...
}
for ; b != nil; b = b.overflow(t) {
for i, k := uintptr(0), b.keys(); i < bucketCnt; i, k = i+1, add(k, 8) {
if *(*uint64)(k) == key && b.tophash[i] != empty {
return add(unsafe.Pointer(b), dataOffset+bucketCnt*8+i*uintptr(t.valuesize))
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
複製程式碼
獲取value值的過程略寫了,過程與存value值類似。首先找到bucket的位置,取得bmap的指標,然後開始遍歷查詢是否存在這個key,有就返回存放value的指標值,否則返回nil。