简介
在 Go 语言中,Map 是一种键值对数据结构,类似于其它语言中的字典或哈希表,用于存储无序的键值对数据,其中每个键唯一对应一个值。
Map 的定义形式为:map[keyType]valueTyp
// 创建一个空的 map
var personMap map[string]int
// 初始化一个 map
personMap = map[string]int{
"Tom": 25,
"John": 32,
"Amy": 19,
}
// 向 map 中添加新的键值对
personMap["Jack"] = 27
// 访问 map 中的元素
fmt.Println(personMap["Tom"]) // 输出:25
// 删除 map 中的元素
delete(personMap, "Amy")
// 遍历 map 中的键值对
for name, age := range personMap {
fmt.Printf("%s is %d years old\n", name, age)
}
上述代码创建了一个 personMap 的 map 变量,其中键为字符串类型,值为整数类型,表示人名和年龄的键值对。
接着通过 map[key] = value 的方式向 map 中添加新的键值对,通过 map[key] 的方式访问 map 中的元素。此外,还可以通过 delete(map, key) 的方式删除 map 中的元素,通过 for range 的方式遍历 map 中的键值对
底层实现
Go语言中的Map使用哈希表(Hash Table)来实现。哈希表是一种由键值对组成的数据结构,它可以支持常数级别的插入、删除和查找操作,因此在处理大量数据时非常高效。
在Go语言中,Map底层实现是一个哈希表数组。每个哈希表包含了若干个桶(Bucket),每个桶中存储了若干个键值对。Map中的每个键值对存储在一个Entry结构体中,其中Key字段是键,Value字段是值。哈希表中的每个桶是一个链表,每个链表节点都是一个Entry结构体。
以下是一个简单的 Entry 结构体的代码实现示例
type Entry struct {
Key string // 键
Value interface{} // 值
Hash uint64 // 哈希值
Next *Entry // 指向链表中下一个元素的指针
}
这个 Entry 结构体包含了四个字段:
Key 表示键,使用 string 类型来存储。
Value 表示值,使用 interface{} 类型来存储,这样可以存储任何类型的值。
Hash 表示哈希值,使用 uint64 类型来存储,这是为了支持大量数据的快速哈希计算。
Next 表示链表中下一个元素的指针,使用 *Entry 类型来存储,这是为了支持链表解决哈希冲突。
这个 Entry 结构体实现了哈希表中的键值对,同时支持解决哈希冲突的链表操作。在使用哈希表时,可以将多个键值对存储在一个 Entry 结构体中,然后将这个结构体存储到哈希表的对应位置中。
下面是一个简单的 map 的底层桶的代码实现示例,其中桶的数量为 16
type Node struct {
key string
value interface{}
next *Node
}
type Map struct {
buckets []*Node
}
func NewMap() *Map {
m := &Map{make([]*Node, 16)}
return m
}
func (m *Map) Get(key string) interface{} {
index := hash(key) % len(m.buckets)
node := m.buckets[index]
for node != nil {
if node.key == key {
return node.value
}
node = node.next
}
return nil
}
func (m *Map) Set(key string, value interface{}) {
index := hash(key) % len(m.buckets)
node := m.buckets[index]
for node != nil {
if node.key == key {
node.value = value
return
}
node = node.next
}
newNode := &Node{key, value, m.buckets[index]}
m.buckets[index] = newNode
}
func hash(key string) int {
h := 0
for i := 0; i < len(key); i++ {
h = 31*h + int(key[i])
}
return h
}
在这个实现中,使用了一个 Map 结构体,其中有一个 buckets 成员变量,表示桶的数组,每个桶是一个链表,用 Node 结构体表示。Set 方法将键值对添加到桶中,如果桶中已经存在该键,就更新对应的值;如果桶中不存在该键,就添加一个新的节点到链表的头部。Get 方法通过键获取值,根据键计算出桶的索引,然后在链表中查找该键对应的节点,如果找到了就返回节点的值。hash 函数使用了一个简单的哈希算法来将字符串键映射到整数索引。
当插入一个键值对时,首先需要计算键的哈希值,然后根据哈希值找到对应的哈希表和桶。如果桶为空,则直接将键值对插入桶中;否则需要遍历桶中的链表,查找键是否已经存在。如果键已经存在,则更新对应的值;否则将键值对插入链表的末尾。
当查找一个键值对时,也需要先计算键的哈希值,然后根据哈希值找到对应的哈希表和桶。然后遍历桶中的链表,查找键对应的值。
当删除一个键值对时,也需要先计算键的哈希值,然后根据哈希值找到对应的哈希表和桶。然后遍历桶中的链表,查找键对应的节点,并删除节点。
总的来说,Go语言中的Map底层实现是非常高效的,可以在常数级别的时间内完成插入、删除和查找操作。但是需要注意的是,在使用Map时需要保证键类型的可比较性,否则程序会在运行时报错。
以下是Go语言中map的部分实现代码(摘自Go语言1.16.5版本)
type hmap struct {
count int // map中的元素数量
flags uint8 // 可以存储一些标记信息,例如map是否被锁定等
B uint8 // 桶数量的对数,即2^B个桶
noverflow uint16 // 溢出桶数量
hash0 uint32 // hash种子,用于计算哈希值
buckets unsafe.Pointer // 桶指针,指向底层哈希表的桶数组
oldbuckets unsafe.Pointer // 指向老的桶数组,用于rehash
nevacuate uintptr // 下一次evacuate过程的位置
extra *mapextra // 一些额外信息,例如指向溢出桶的指针
}
type mapextra struct {
overflow *[]*bmap // 溢出桶指针数组
oldoverflow *[]*bmap // 老的溢出桶指针数组,用于rehash
nextOverflow *bmap // 下一个要分配的溢出桶
}
type bmap struct {
tophash [bucketCnt]uint8
// data: 记录了桶中所有key-value对的信息
// 每个key-value对由1个key和1个value组成,key和value的类型都是interface{},但是实际上key和value的类型是具体的类型,而不是interface{}
// key和value都需要通过interface转换才能被使用
data [bucketCnt]struct {
key, value unsafe.Pointer
}
// 一个bucket可能会分配多个溢出桶,用overflow字段指向第一个溢出桶
overflow *bmap
}
网友评论