map

2023-04-01  本文已影响0人  我是聪

简介

在 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
}


上一篇 下一篇

猜你喜欢

热点阅读