1.什么是map
map是Go语言提供的一种抽象数据类型.它表示一组无序的键值对.map对value的类型没有限制.但是对key有严格的要求.key的类型应该严格定义了作为"=="和"!="两个操作符的操作数时的行为,因此函数 map 切片不能作为map的key类型.map类型不支持"零值可用".未显示赋初值的map类型变量的零值为nil.对处于零值状态的map变量进行操作将会导致panic.
func main() {
//m为nil.
var m map[int]string
m[1] = "hello"
}
必须对map类型变量进行显示初始化后才能使用它,和切片一样.创建map类型变量有两种方式.一种是使用符合字面值.另一种是使用make这个预声明的内置函数.
使用复合字面值创建map类型变量:
func main() {
//m为nil.
var m = map[int]string{
1: "hello",
2: "go",
}
m[3] = "world"
fmt.Println(m)
}
使用make创建map类型变量:
func main() {
m := make(map[int]string)
m[1] = "hello"
m[2] = "world"
m[3] = "go"
fmt.Println(m)
}
和切片一样.map也是引用类型.将map类型变量作为函数参数传入不会有很大性能消耗.并且在函数内部对map变量的修改在函数外部也是可见的.
func foo(m map[string]int) {
m["key1"] = 11
m["key2"] = 22
}
func main() {
m := map[string]int{
"key1": 1,
"key2": 2,
}
fmt.Println(m)
foo(m)
fmt.Println(m)
}
2.map的基本操作:
1).插入数据:
面对一个非nil的map类型变量.可以向其插入符合map类型定义的任意键值对.Go运行时会负责map内部的内存管理.除非是系统内存耗尽.所以不用担心map中插入数据的数量.
m := make(map[K]V)
m[k1] = v1
m[k2] = v2
m[k3] = v3
如果key已经存在于map中.则该插入操作会用新值覆盖旧值.
m := map[string]int {
"key1" : 1,
"key2" : 2,
}
//11会覆盖掉旧值1.
m["key1"] = 11
m["key23"] = 3
2).获取数据个数:
和切片一样.map也可以通过内置函数len获取当前已存储的数据个数.
m := map[string]int{
"key1" : 1,
"key2" : 2,
}
//2.
fmt.Println(len(m))
m["key3"] = 3
//3.
fmt.Println(len(m))
3).查找和数据读取:
map类型更多用在查找和数据读取场合.所谓查找就是判断某个key是否存在于某个map中.可以使用"comma ok"惯用法进行查找.
_,ok := m["key"]
if !ok {
//"key"不在map中.
}
如果并不关心某个key对应的value.仅仅关心某个key是否在map中.使用空标识符(blank identifier)忽略了可能返回的数据值.仅关心ok的值是否为true(表示在map中).
如果要读取key对应的value值.
m := map[string]int
m["key1"] = 1
m["key2"] = 2
v := m["key1"]
//1.
fmt.Println(v)
v := m["key3"]
//0.
fmt.Println(v)
对于map中不存在的值还是返回了一个默认值.在这样的情况下无法判断这个值是存在不存在.因此还是要借助"comma ok"惯用法.只有当ok=true时.所得的value值才是需要的.
4).删除数据:
借助内置函数delete从map中删除数据.
m := map[string]int {
"key1" : 1,
"key2" : 2,
}
fmt.Println(m)
delete(m,"key2")
fmt.Println(m)
注意:即使要删除的数据在map中不存在.delete也会导致panic.
5).遍历数据:
可以像对待切片那样通过for range语句对map中的数据进行遍历:
func main(){
m := map[int]int{
1 : 11,
2 : 12,
3 : 13,
}
fmt.Printf("{")
for k,v := range m {
fmt.Printf("[%d,%d]",k,v)
}
fmt.Printf("}\n")
}
注意:Go在运行时在初始化map迭代器时对起始位置做了随机处理,千万不要依赖map所得到的元素次序.
3.map的内部实现:
和切片相比.map类型的内部实现要复杂的多.Go运行时用一张哈希表来实现抽象的map类型.运行时实现了map操作的所有功能.包括查找 插入 删除和遍历等.在编译阶段.Go编译器会将语法层面的map操作重写成运行时对应的函数调用.
m := make(map[keyType]valType,capacityhint) -> m := runtime.makemap(maptype,capacityhint,m)
v := m["key"] -> v := runtime.mapaccess1(maptype,m,"key")
v , ok := m["key"] -> v , ok := runtime.mapassign(maptype, m, "key") //v是用于后续存储value的空间的地址.
delete(m,"key") -> runtime.mapdelete(maptype,m,"key")
1).初始状态:
count:当前map的元素个数.对map类型变量运用len内置函数时.len返回的就是count值.
flags:当前map所处的状态标志.
B:B的值是bucket数量的以2为底的对数.
noverflow:overflow bucket的大约数量.
buckets:哈希函数的种子值.
oldbuckets:在map扩容阶段指向前一个bucket数组的指针.
nevacuate:在map扩容阶段充当扩容进度计数器.所有下标号小于nevacuate的bucket都已完成了数据排空和迁移操作.
extra:可选字段.如果有overflow bucket存在.且key value都因不包含指针而被内联的情况下.该字段将存储所有指向overflow bucket指针.保证overflowbucket始终可用.(不被垃圾回收掉).
真正用于存储键值对数据的是bucket(桶).每个bucket中存储的是Hash值低于bit位数值相同的元素.默认的元素个数为BUCKETSIZE(值为8).当某个bucket的8个空槽(slot)都已填满且map尚未达到扩容条件时.运行时会建立overflow bucket.并将该overflow bucket挂在上面bucket末尾的overflow指针上.这样两个bucket形成了一个链表结构.该结构的存在将持续到下一次map扩容.
每个bucket由三部分组成.tophash区域 key存储区域和value存储区域.
tophash区域:
当向map插入一条数据或从map按key查询数据的时候.运行时会使用哈希函数对key做哈希运算并获得一个哈希值的hashcode.这个hashcode很关键.运行时将hashcode一分为二看待.低位区的值用于选定bucket.高位区用于某个bucket中确定key的位置.因此,每个bucket的tophash区域是用于快速定位key位置的.这样避免了逐个key进行比较这种代价的操作.尤其是key是size较大的字符串类型.是一种空间换时间的思路.
key存储区域:
tophash区域下面是一块连续的内存区域.存储的是bucket承载的所有key数据.运行时在分配bucket时需要知道key的大小.Go运行时会为该变量对应的特定map类型生成一个maptype实例(如存在.则复用):
type mapType struct {
abi.OldMapType
}
type OldMapType struct {
Type
Key *Type
Elem *Type
Bucket *Type // internal type representing a hash bucket
// function for hashing keys (ptr to key, seed) -> hash
Hasher func(unsafe.Pointer, uintptr) uintptr
KeySize uint8 // size of key slot
ValueSize uint8 // size of elem slot
BucketSize uint16 // size of bucket
Flags uint32
}
上面结构体包含了map类型的所有元信息.前面提到过编译器会将语法层面的map操作重写成运行时所对应的函数调用..这些运行时函数有一个共同特定.第一个参数都是maptype指针类型的参数.Go运行时就是利用maptype参数中的信息确定key的类型和大小的.map所用的hash函数也存放在maptype.key.alg.hash(key,hmap.hash())中.同时maptype的存在也让Go中所有的map类型共享一套运行时的map操作函数.减少了对最终二进制文件空间的占用.
value存储区域:
key存储区域下方是一块连续的内存区域.该区域存储的是key对应的value.和key一样.该区域的创建也得到了了maptype信息的帮助.Go运行时采用了将key和value分开存储而不是采用一个kv接着一个kv紧邻方式存储.带来了算法上的复杂性.减少了内存对齐带来的内存浪费.
如果key或value的数据长度大于一定的数值.那么运行时不会在bucket中直接存储数据.而是会存储key或value数据的指针.
4.map的扩容:
map会对底层使用的内存进行自动管理.在使用过程中.在插入元素超出一定数值后.map势必存在自动扩容的问题(扩充bucket数量).并重新在bucket间均衡分配数据.
Go在运行时的map实现中引入了一个LoadFactor(负载因子).当count>LoadFactor*2^B或overflow bucket过多时.运行时会对map进行扩容.目前LoadFactor设置为6.5.
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := sys.GetCallerPC()
pc := abi.FuncPCABIInternal(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.Key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.Key.Size_)
}
if asanenabled {
asanread(key, t.Key.Size_)
}
if h.flags&hashWriting != 0 {
fatal("concurrent map writes")
}
hash := t.Hasher(key, uintptr(h.hash0))
// Set hashWriting after calling t.hasher, since t.hasher may panic,
// in which case we have not actually done a write.
h.flags ^= hashWriting
if h.buckets == nil {
h.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)
}
again:
bucket := hash & bucketMask(h.B)
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
for {
for i := uintptr(0); i < abi.OldMapBucketCount; i++ {
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
elem = add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
}
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
if t.IndirectKey() {
k = *((*unsafe.Pointer)(k))
}
if !t.Key.Equal(key, k) {
continue
}
// already have a mapping for key. Update it.
if t.NeedKeyUpdate() {
typedmemmove(t.Key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+abi.OldMapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// Did not find mapping for key. Allocate new cell & add entry.
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
if inserti == nil {
// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, abi.OldMapBucketCount*uintptr(t.KeySize))
}
// store new key/elem at insert position
if t.IndirectKey() {
kmem := newobject(t.Key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.IndirectElem() {
vmem := newobject(t.Elem)
*(*unsafe.Pointer)(elem) = vmem
}
typedmemmove(t.Key, insertk, key)
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
fatal("concurrent map writes")
}
h.flags &^= hashWriting
if t.IndirectElem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
流程:
开始
↓
安全检查(nil map/并发写/竞赛检测)
↓
计算 key 哈希值
↓
标记:正在写入(防止并发)
↓
如果桶未初始化 → 创建第一个桶
↓
循环:
- 计算目标桶编号
- 如果正在扩容 → 先迁移数据
- 遍历当前桶 + 溢出桶
↳ 找到相同 key → 更新,直接结束
↳ 找到空位 → 记录插入位置
↳ 桶满 → 找下一个溢出桶
↓
没找到 key:
↳ 达到负载因子 → 扩容 → 重新从头执行
↳ 没有空位 → 新建溢出桶
↳ 写入新 key/value
↳ count++
↓
取消写入标记
↓
返回 value 指针
结束
如果是因为overflow bucket过多导致的"扩容".实际上运行时会新建一个和现有规模一样的bucket数组.然后在进行assign和delete操作的时候进行排空和迁移.如果因为当前数据量超出LoadFactor指定的水位情况.那么运行时会建立一个两倍于现有规模的bucket数组.但真正排空和迁移工作也是进行assign和delete操作逐步进行的.原bucket数组会挂在hmap的oldbuckets指针下面.知道原bucket数组中的所有数据都迁移到新数组.原bucket数组才会被释放.
5.map与并发:
从上面实现原理来看.充当map描述符角色的hmap实例自身是有状态的(hmap.flags)且对状态的读写是没有并发保护的.因此map实例不是并发安全的.不支持并发读写.如果对map实例进行并发读写会发生panic.
func main() {
m := map[int]int{
1: 11,
2: 12,
3: 13,
}
go func() {
for i := 0; i < 1000; i++ {
doIteration(m)
}
}()
go func() {
for i := 0; i < 1000; i++ {
doWrite(m)
}
}()
time.Sleep(5 * time.Second)
}
func doIteration(m map[int]int) {
for k, v := range m {
_ = fmt.Sprintf("[%d:%d]", k, v)
}
}
func doWrite(m map[int]int) {
for k, v := range m {
m[k] = v + 1
}
}
6.尽量使用cap参数创建map:
从上面自动扩容原理了解到.如果初始创建map没有创建足够多可以应付map使用场景的bucket.那么随着插入map元素数量增多.map会频繁扩容.而这一过程将降低map的访问性能.如果可能的话.最好对map使用规模做出粗略的估算.并使用cap参数对map实例进行初始化.
func BenchmarkMapInitWithoutCap(b *testing.B) {
for n := 0; n < b.N; n++ {
m := make(map[int]int)
for i := 0; i < 10000; i++ {
m[i] = i
}
}
}
func BenchmarkMapInitWithCap(b *testing.B) {
for n := 0; n < b.N; n++ {
m := make(map[int]int,10000)
for i := 0; i < 10000; i++ {
m[i] = i
}
}
}
谢家庭院残更立.燕宿雕梁.月度银墙.不辩花丛那辩香.
此情已自成追忆.零落鸳鸯.雨歇微凉.十一年前梦一场. 纳兰.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路