1、初始化一个map变量

	m1 := make(map[string]interface{})
	m2 := make(map[string]interface{}, 10)

2、上面两个make,会调用对应的实现方法。
        当我们使用make来初始化一个map变量时,
        1、如果不指定数量,会执行makemap_small()并返回一个hmap的地址,
        2、如果指定数量,会调用makemap64()并返回一个hmap地址,

//以下方法均来自源码runtime/map.go文件
func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

func makemap64(t *maptype, hint int64, h *hmap) *hmap {
	if int64(int(hint)) != hint {
		hint = 0
	}
	return makemap(t, int(hint), h)
}
func makemap(t *maptype, hint int, h *hmap) *hmap {
	//````省略一些代码
	if h == nil {
		h = new(hmap)
	}
    h.hash0 = fastrand()
	//````省略一些代码
	return h
}

3、看一下makemap()函数中做了哪些动作

暂时我们先记住,当make一个map时,它会去new一个hmap给我们,现在我们看一下makemap()方法的完整代码。

源码中的B,就是bucket,可以称之为“桶”,桶的意思就是存储数据的,每个桶都可以装8个键值对。

如果我们在make一个指定数量的map时,就会调用makemap()函数,并且根据指定的数量,在 overLoadFactor(hint, B)时,计算出来应该生成多少个桶,并且在makeBucketArray(t, h.B, nil)方法开始分配内存,生成对应数量的桶。

func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    // 这个只是校验,并不关键,这个校验如果数量超过最大值,就给它重置为0不分配桶了
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()

	// Find the size parameter B which will hold the requested # of elements.
	// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// allocate initial hash table
	// if B == 0, the buckets field is allocated lazily later (in mapassign)
	// If hint is large zeroing this memory could take a while.
	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
}

4、研究一下hmap结构体

我们在第2步得知,当make一个map时,它会去new一个hmap给我们,hmap实际上就是哈希map,下面我们再来研究一下这个hmap

在runtime/map.go这个源码文件中,可以找到hmap这个结构体的定义

 hmap结构体中,几个关键字段:

        count:是存储了多少个数据

        B:桶的数量是log_2的多少。如果是B=0,桶的数量对应的就是2^0 = 1个,B=1,那2^1 = 2个,以此类推。

        noverflow: 溢出桶的数量

        buckets:是桶的地址

        oldbuckets:旧桶的地址(如果桶满了会产生扩容,数据会从旧桶迁移到新桶)

        nevacuate: 迁移新桶时记录的下一个要被迁移的旧桶编号

        extra:里面会记录一些溢出桶的信息(当一个桶满了,hash运算还往桶里装数据时会放进溢出桶)

5、谈一谈存储过程

        5.1、普通存储:

        5.2、触发扩容条件:

        5.3、桶满了但没触发扩容条件:

        

        普通存储:

        假设我们在make的初始化的时候,B=2,也就是说分配了4个桶,那么4个桶的编号分别是0,1,2,3。当我们添加一个键值对时,应该把这个数据放在哪个桶里呢?

        这就设计到hash内在的算法了。比如取模法,先把键值对的key进行hash运算,hash%桶的数量取模,得到应该放到哪个桶里。取值的时候也是一样。

        触发扩容条件:

        map的扩容是翻倍扩容,比如现在B=2,相对应有4个桶,当触发扩容机制的条件时overLoadFactor(hint, B),存储数量/桶的数量>6.5,就会翻倍扩容分配8个新桶。此时hmap中的oldbucket就会记录原来的桶的地址,bucket记录新桶的地址,并且旧桶的数据开始往新桶迁移,迁移的进度会被nevacuate记录,但要注意,map迁移时是用的渐进式迁移,不会一下子全部迁移,那样数据量过大时会有性能影响,所以每次只会迁移一两个桶。

        桶满了但没触发扩容条件:

        有时候会有这样的情况,当hash运算时,总是往某一个桶里增加数据,以至于桶都满了。这时候溢出桶就会起作用了。当新的数据存储到某个满桶时,这个桶就会链出一个新桶,新桶还可以继续链出新新桶,形成一个链条式的。当查询时也是,经过hash算法找到这个桶发现没有那个key,通过链条找一下溢出桶对比key值是否存在,一直把它找到。