什么是map
是由一组<key,value>对组成的数据结构,并且同一个key只能出现一次
有map相关的操作主要是
- 增加一个k-v对------Add or insert
- 删除一个k-v对------Remover or delete
- 修改某个k对应的v------Reassign
- 查询某个k对应的v------Lookup
- map的设计也称为
The dictionary problem
,它的任务是设计一种数据结构用来维护一个集合的数据,并且可以同时对集合进行增删改查的操作。最主要的数据结构有两种:哈希查找表(Hashtable)、 搜索树(Searchtree)
哈希查找表用一个哈希函数将 key 分配到不同的桶(bucket,也就是数组的不同 index
)。这样,开销主要在哈希函数的计算以及数组的常数访问时间。在很多场景下,哈希查找表的性能很高。
哈希查找表一般会存在"碰撞"的问题,就是说不同的key被哈希到同一个bucket(bucket :数组的不同index
)。一般有两种应对的方法:链表法和开放地址法
。链表法
将同一个bucket
实现成一个链表,落在同一个bucket
中的key
都会插入这个链表。开放地址法
则是碰撞发生后,通过一定的规律,在数组的后面挑选"空位"用来放置新的key
搜索树法一般采用自平衡搜索树,包括:AVL 树,红黑树。
自平衡搜索树法的最差搜索效率是 O(logN),而哈希查找表最差是 O(N)。
遍历自平衡搜索树,返回的 key 序列,一般会按照从小到大的顺序;而哈希查找表则是乱序的。
为什么要用 map
Go语言内置的map实现了
hash table
,特点是实现了快速查找,添加,删除的功能。
map的底层实现
在源码中,表示map
的结构体是hmap
,他是hashmap
的"缩写":
// A header for a Go map.
type hmap struct {
// 元素个数,调用 len(map) 时,直接返回此值
count int
flags uint8
// buckets 的对数 log_2
B uint8
// overflow 的 bucket 近似数
noverflow uint16
// 计算 key 的哈希的时候会传入哈希函数
hash0 uint32
// 指向 buckets 数组,大小为 2^B
// 如果元素个数为0,就为 nil
buckets unsafe.Pointer
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr
extra *mapextra // optional fields
}
说明一下, B 是 buckets 数组的长度的对数,也就是说 buckets 数组的长度就是 2^B。
bucket 里面存储了 key 和 value,后面会再讲。
buckets 是一个指针,最终它指向的是一个结构体:
type bmap struct {
tophash [bucketCnt]uint8
}
但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它加料,动态地创建一个新的结构:
type bmap struct {
topbits [8]uint8
keys [8]
keytype values [8]valuetype
pad uintptr
overflow uintptr
}
bmap 就是我们常说的“桶”,桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。
来一个整体的图:
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
type mapextra struct {
// overflow[0] contains overflow buckets for hmap.buckets.
// overflow[1] contains overflow buckets for hmap.oldbuckets.
overflow [2]*[]*bmap
// nextOverflow 包含空闲的 overflow bucket,这是预分配的 bucket
nextOverflow *bmap
}
bmap 是存放 k-v 的地方,我们把视角拉近,仔细看 bmap 的内部组成。
上图就是 bucket 的内存模型, HOBHash 指的就是 top hash。注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/… 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。
例如,有这样一个类型的 map:
map[int64]int8
如果按照 key/value/key/value/… 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 key/key/…/value/value/…,则只需要在最后添加 padding。
每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 overflow 指针连接起来。
创建 map
ageMp:=make(map[string]int)
//指定map长度
ageMp:=make(map[string]int,8)
//ageMp 为nil,不能向其添加元素,否则会直接panic
通过汇编语言可以看到,实际上底层调用的是 makemap 函数,主要做的工作就是初始化 hmap 结构体的各种字段,例如计算 B 的大小,设置哈希种子 hash0 等等。
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
注意,这个函数返回的结果:*hmap,它是一个指针,而我们之前讲过的 makeslice 函数返回的是 Slice 结构体:
func makeslice(et *_type,len ,cap int)slice
回顾一下 slice 的结构体定义:
type slice struct{
array unsafe.Pointer//元素指针
len int// 长度
cap int// 容量
}
结构体内部包含底层的数据指针。
makemap 和 makeslice 的区别,带来一个不同点:当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对 slice 却不会(之前讲 slice 的文章里有讲过)。
主要原因:一个是指针( *hmap),一个是结构体( slice)。Go 语言中的函数传参都是值传递,在函数内部,参数会被 copy 到本地。*hmap指针 copy 完之后,仍然指向同一个 map,因此函数内部对 map 的操作会影响实参。而 slice 被 copy 后,会成为一个新的 slice,对它进行的操作不会影响到实参。
哈希函数
key 定位过程
以上省略了好大一部分的讲解~~
因为看不懂,后续再来补充~
原文地址
map 的两种 get 操作
Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。
package main
import (
"fmt"
)
func main() {
ageMap :=make(map[string]int)
ageMap["fu"] =26
//不带comma用法
agel :=ageMap["shao"]
fmt.Println(agel)
//带comma用法
age2,ok :=ageMap["shao"]
fmt.Println(age2,ok)
}
运行结果:
0
0 false
以前一直觉得好神奇,怎么实现的?这其实是编译器在背后做的工作:分析代码后,将两种语法对应到底层两个不同的函数。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
源码里,函数命名不拘小节,直接带上后缀 1,2,完全不理会《代码大全》里的那一套命名的做法。从上面两个函数的声明也可以看出差别了, mapaccess2 函数返回值多了一个 bool 型变量,两者的代码也是完全一样的,只是在返回值后面多加了一个 false 或者 true。
如何进行扩容
以上省略了好大一部分的讲解~~
因为看不懂,后续再来补充~
原文地址
map 的遍历
package main
import (
"fmt"
)
func main() {
ageMap :=make(map[string]int)
ageMap["fu"] =26
for name,age :=range ageMap{
fmt.Println(name,age)
}
}
执行汇编命令:
go tool compile -S main.go
关键的几行汇编代码如下:
这样,关于 map 迭代,底层的函数调用关系一目了然。先是调用 mapiterinit 函数初始化迭代器,然后循环调用 mapiternext 函数进行 map 迭代。
以上省略了好大一部分的讲解~~
因为看不懂,后续再来补充~
原文地址
map 的赋值
package main
import (
"fmt"
)
func main() {
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 666
scene["china"] = 960
for k, v := range scene {
fmt.Println(k, v)
}
}
map 的删除
package main
import (
"fmt"
)
func main() {
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
fmt.Println(k, v)
}
}
可以边遍历边删除吗
go语言map遍历时删除是安全的, 且可以完全删除
package main
import (
"fmt"
)
func main() {
x := map[int]int{ }
for i := 0; i < 10000; i++ {
x[i] = i
}
fmt.Println("初始化后,长度:", len(x))
// 遍历时删除所有的偶数
for k := range x {
if k%2 == 0 {
delete(x, k)
}
}
fmt.Println("删除所有的偶数后,长度:", len(x))
// 遍历时删除所有的元素
for k := range x {
delete(x, k)
}
fmt.Println("删除所有的元素后,长度:", len(x))
}
map并发读写
map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。
package main
import (
"fmt"
"time"
)
func main(){
c := make(map[string]int)
go func() { //开一个goroutine写map
for j := 0; j < 1000000; j++ {
c[fmt.Sprintf("%d", j)] = j
}
}()
go func() { //开一个goroutine读map
for j := 0; j < 1000000; j++ {
fmt.Println(c[fmt.Sprintf("%d",j)])
}
}()
time.Sleep(time.Second*20)
}
一般而言,这可以通过读写锁来解决:sync.RWMutex。
读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。
package main
import (
"fmt"
"time"
"sync"
)
func main(){
c := make(map[string]int)
var rwLock *sync.RWMutex = new(sync.RWMutex)
go func() { //开一个goroutine写map
for j := 0; j < 10; j++ {
rwLock.RLock()
c[fmt.Sprintf("%d", j)] = j
rwLock.RUnlock()
}
}()
go func() { //开一个goroutine读map
for j := 0; j < 10; j++ {
rwLock.Lock()
fmt.Println(c[fmt.Sprintf("%d",j)])
rwLock.Unlock()
}
}()
time.Sleep(time.Second*1)
}
另外, sync.Map 是线程安全的 map,也可以使用。它的实现原理,这次先不说了。
sync.map的使用
key 可以是 float 型吗?
从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions
这几种类型,其他类型都是 OK 的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 == 和 != 操作符, k1==k2 时,可认为 k1 和 k2 是同一个 key。如果是结构体,则需要它们的字段值都相等,才被认为是相同的 key。
顺便说一句,任何类型都可以作为 value,包括 map 类型