在Go语言中,map
是一种非常强大的数据结构,它提供了键值对的存储和快速查找功能。本文将详细介绍Go语言中map
的数据结构、实现原理和常见操作。
Go语言中的map
数据结构由runtime/map.go
中的hmap
定义:
type hmap struct {
count int // 当前保存的元素个数
B uint8 // bucket的数量是2^B
buckets unsafe.Pointer // bucket数组指针,数组的大小为2^B
...
}
在Go语言中,map
是一种哈希表的实现,它通过哈希函数将键映射到数组中的位置,以实现快速的查找和插入操作。hmap
结构体是map
的核心数据结构,包含了哈希表的基本信息和指向bucket
数组的指针。
每个bucket
可以存储8个键值对,bucket
的数据结构定义如下:
type bmap struct {
tophash [8]uint8 // 存储哈希值的高8位
data byte[1] // key和value数据:key/key/key/.../value/value/value...
overflow *bmap // 溢出bucket的地址
}
tophash:tophash
是一个长度为8的数组,用于存储每个键的哈希值的高8位。在哈希表中,多个键可能会映射到相同的bucket
,这就会导致哈希冲突。为了快速判断一个键是否在当前bucket
中,tophash
数组存储了每个键的哈希值的高位部分。当我们查找一个键时,首先计算键的哈希值,然后取哈希值的高8位在tophash
数组中查找匹配项。如果找到匹配项,再进一步比较键和值。
data:data
区存放的是键值对的数据,存放顺序是key/key/key/.../value/value/value
。这种存放方式可以节省字节对齐带来的空间浪费。因为在内存中,数据的对齐可能会浪费掉一些空间,通过这种紧凑的存储方式,可以更高效地利用内存。
overflow:overflow
指针指向下一个溢出的bucket
,用于处理哈希冲突。当一个bucket
满了之后,新元素会存到溢出的bucket
中。通过这种链式结构,可以处理多个哈希冲突的键值对。
每个哈希表的实现对负载因子的容忍程度不同。负载因子是指哈希表中元素的数量与bucket
数量的比值。例如,Redis在负载因子大于1时就会触发rehash
,而Go在负载因子达到6.5时才会触发rehash
。这是因为Redis的每个bucket
只能存1个键值对,而Go的bucket
可以存8个键值对,因此Go可以容忍更高的负载因子。
当负载因子过高时,哈希表的性能会下降,因为哈希冲突会增加,查找和插入操作的时间复杂度会变高。为了保持高效的性能,哈希表需要在负载因子达到一定阈值时进行扩容和rehash
。
map
的查找过程如下:
计算哈希值:根据key
值计算哈希值。Go语言使用了高效的哈希函数来计算键的哈希值,这个哈希值是一个固定长度的整数,它将键映射到哈希表中的某个位置。
确定bucket位置:取哈希值的低位与hmap.B
取模,确定bucket
的位置。哈希值的低位部分决定了键在bucket
数组中的位置,这样可以快速定位到对应的bucket
。
查找tophash:取哈希值的高位,在tophash
数组中查询。tophash
数组存储了每个键的哈希值的高8位,通过查找tophash
数组,可以快速判断键是否在当前bucket
中。
比较键值:如果tophash[i]
中的值与哈希值相等,则找到该bucket
中的key
值进行比较。如果键值相同,则查找成功。
处理溢出bucket:如果当前bucket
没有找到,则继续从下一个overflow
的bucket
中查找。通过链式结构,可以处理多个哈希冲突的键值对。
搬迁查找:如果当前处于搬迁过程,则优先从oldbuckets
查找。在哈希表进行rehash
时,可能会同时存在旧的bucket
数组和新的bucket
数组,此时需要优先从旧的bucket
数组中查找。
注意:如果查找不到,不会返回空值,而是返回相应类型的零值。例如,如果查找一个不存在的整数键,返回值是0
。
新元素的插入过程如下:
计算哈希值:根据key
值计算哈希值。
确定bucket位置:取哈希值的低位与hmap.B
取模,确定bucket
的位置。
查找键值:查找该key
是否已经存在,如果存在则直接更新值。
插入新键值:如果没找到key
,则将key
插入。如果当前bucket
已满,则需要使用overflow
链进行处理,将新元素插入到溢出的bucket
中。
声明但不初始化:
var m1 map[int]string
此时m1
是nil
,不能存储数据,因为没有分配内存并初始化内部数据结构。如果尝试直接使用,会引发运行时错误。
使用字面量初始化:
m2 := map[int]string{}
这种方式创建了一个空的map
,可以直接存储数据。
使用make函数创建:
m3 := make(map[int]string)
创建一个空的map
,默认len=0
。
指定初始容量:
m4 := make(map[int]string, 10)
创建时指定初始容量为10,这样可以避免在初始插入时多次扩容,提高性能。
使用字面量初始化:
var m map[int]string = map[int]string{1: "aa", 2: "bbb"}
保证key
彼此不重复。
使用简短声明初始化:
m := map[int]string{1: "aaa", 2: "bbb"}
赋值过程中,如果新map
元素的key
与原map
的key
相同,则替换;如果不同,则添加。
map
使用range
关键字可以遍历map
中的所有键值对:
for key, value := range m {
fmt.Println(key, value)
}
如果只需要遍历键,可以省略值:
for key := range m {
fmt.Println(key)
}
map
中key
是否存在map
的下标运算返回两个值,第一个是value
的值(如果不存在则为零值),第二个是key
是否存在的布尔类型。
value, exists := m[key]
if exists {
fmt.Println("Key exists with value", value)
} else {
fmt.Println("Key does not exist")
}
map
中的元素使用delete()
函数删除元素:
delete(m, key)
删除一个不存在的key
不会报错:
delete(m, nonExistentKey)
map
作为函数参数和返回值map
在作为函数参数和返回值时,传递的是引用。这意味着在函数内部对map
的修改会影响到函数外部的map
。
func modifyMap(m map[int]string) {
m[1] = "modified"
}
func main() {
m := map[int]string{1: "original"}
modifyMap(m)
fmt.Println(m[1]) // 输出 "modified"
}
map
不是线程安全的。如果在多个协程中并发读写同一个map
,需要使用互斥锁(sync.Mutex
)或者读写锁(sync.RWMutex
)来保证线程安全。map
的键类型必须是可比较的类型,例如整数、浮点数、字符串、指针、结构体等。不能使用切片、map
、函数等不可比较的类型作为键。map
的值类型可以是任意类型,包括内置类型、自定义类型、切片、map
、指针等。Go语言中的map
是一种高效的键值对存储结构,通过哈希表实现快速查找和插入操作。它具有以下特点:
bucket
实现快速查找和插入操作,时间复杂度接近O(1)。bucket
和链式结构解决哈希冲突,保证较高的性能。rehash
操作,当负载因子过高时自动扩容,保持高效性能。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论