Go map
基础
import (
"fmt"
"testing"
)
// 声明map
var m = map[string]interface{}{}
// 初始化
func TestInitUseMap(t *testing.T) {
// 使用make来进行初始化
m = make(map[string]interface{})
// 增加
m["key1"] = "v1"
// 查询
v,ok := m["key1"]
if ok {
fmt.Print(v)
}
// 更新
m["key1"] = "v2"
// 删除
delete(m,"key1")
// 遍历
m["k"] = "v"
for k,v := range m{
fmt.Println(k,v)
}
// 函数传参中,map传递的是地址
}
原理
底层数据结构
map结构
// A header for a Go map.
type hmap struct {
count int // 键值对数目
flags uint8 // 是否在扩容
B uint8 // 桶的数目,2的b次方
noverflow uint16 // 溢出桶使用的数量
hash0 uint32 // hash seed
buckets unsafe.Pointer // 桶指针
oldbuckets unsafe.Pointer // 迁移过程中,指向还没迁移完成的旧桶
nevacuate uintptr // 即将迁移的旧桶编号
extra *mapextra // 扩展字段
}
Bmap结构:
- 一个bmap就是一个桶,一个桶里可以放8对键值对
- 为了使内存排列更加紧凑,8个key放一起,8个value放一起
- overflow指针是一个指向溢出桶的指针,为了减少扩容次数,当8个健值对都放满后,对出来的kv就放在溢出桶,溢出桶内存布局和普通桶是一样的
- 桶中还存放在tophash,hash值的高8位
bmap在go源码中结构很简单
type bmap struct {
tophash 【bucketCnt】uint8
}
但是能根据编译期间的 cmd/compile/internal/gc.bmap 函数对它的结构重建
type bmap struct {
topbits 【8】uint8 // tophash
keys 【8】keytype // key数组,一起排列
values 【8】valuetype // value数组,一起排列
pad uintptr
overflow uintptr // 溢出桶指针
}
当哈希表要分配的桶的数目大于2的4次方(16)时,就认为使用到溢出桶的概率比较大,就会预分配2的b-4次方个溢出桶,这些预分配的溢出桶在内存中是和普通通连续排列在一起的。
上边说到的hmap中有一个extra扩展字段,里边是指向mapextra的指针,记录的是溢出桶的使用信息
// mapextra holds fields that are not present on all maps.
type mapextra struct {
overflow *[]*bmap // 是一个切片,记录已经使用的那些溢出桶
oldoverflow *[]*bmap // 记录在扩容阶段存储旧桶用到的那些溢出桶的地址
nextOverflow *bmap // 指向下一个空闲溢出桶
}
扩容规则
go中map默认的负载因子是6.5,负载因子是等于元素个数/hash表容量,负载因子越大,说明空闲位置越少。当满足
count / 2^B > 6.5
就会发生翻倍扩容,map新桶数量是旧桶两倍
如果没有满足大于6.5负载因子,但是使用到了较多的溢出桶,也会发生扩容,但是是等量扩容,就是创建和旧桶数量一样的新桶,在做迁移
B <= 15 && noverflow >= 2^B
or
B > 15 && noverflow >= 2^15
初始化
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 边界检查
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化map
if h == nil {
h = new(hmap)
}
// 生成hash种子
h.hash0 = fastrand()
// 计算得到合适的桶数量B
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 为桶分配空间,即初始化数组出来
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
使用 makeBucketArray创建用于保存桶的数组,这个方法其实就是根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据,在创建桶的过程中还会额外创建一些用于保存溢出数据的桶,数量是 2^(B-4) 个。初始化完成返回hmap指针。
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b)
nbuckets := base
if b >= 4 {
nbuckets += bucketShift(b - 4)
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz {
nbuckets = up / t.bucket.size
}
}
if dirtyalloc == nil {
buckets = newarray(t.bucket, int(nbuckets))
} else {
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.ptrdata != 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
if base != nbuckets {
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}