Golang
主页 > 脚本 > Golang >

Golang Map简介以及底层原理介绍

2024-10-18 | 佚名 | 点击:

在Go语言中提供了map数据结构来存储键值对数据。map的数据类型为map[K]V,其中K为键的类型,V为值的类型。map的键类型必须支持==操作符,用来比较两个键是否相等。Go语言提供了4种内置的map操作: len、delete、comparison、assign。

Map 定义

1

2

3

4

map_var := make(map[K]V) // 用make函数创建一个空的map,其中K和V分别为键和值的类型

map_var[key] = value // 向map中添加一个键值对

value := map_var[key] // 获取指定键的值

delete(map_var, key) // 从map中删除指定的键及其对应的值

Map Iteration

Go语言提供了两个方法来遍历map中的所有键值对,分别是range方法和Len()方法。

1

2

3

4

5

6

7

8

// 使用range循环遍历map中的所有键值对

for key, value := range map_var {

// TODO ...

}

// 计算map中的元素数量

if len(map_var) > 0 {

// TODO ...

}

Map 的线程安全

在Go语言中,map是非线程安全的,在多线程并发访问时可能导致程序报错。当map被多个协程同时访问时,我们需要使用sync包中的sync.Mutex来确保操作的原子性和并发安全。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import "sync"

type SafeMap struct {

mu sync.Mutex

m map[string]int

}

func (sm *SafeMap) Get(key string) int {

sm.mu.Lock()

defer sm.mu.Unlock()

return sm.m[key]

}

func (sm *SafeMap) Set(key string, value int) {

sm.mu.Lock()

defer sm.mu.Unlock()

sm.m[key] = value

}

func (sm *SafeMap) Delete(key string) {

sm.mu.Lock()

defer sm.mu.Unlock()

delete(sm.m, key)

}

map 底层原理

Go语言的map在设计上是一种哈希表的数据结构。它利用哈希函数将键映射到不同的存储空间,从而实现高效的查找和插入操作。

哈希函数

哈希函数将字符串映射到一个整数上,这称为哈希值。不同的字符串可能会有相同的哈希值,但相同的字符串必定具有相同的哈希值。哈希函数需要满足两点:

在Go语言中,字符串的哈希函数采用的是FNV-1哈希算法,算法代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

const (

offset64 = 14695981039346656037

prime64 = 1099511628211

)

func stringHash(s string) uint64 {

h := uint64(offset64)

for i := 0; i < len(s); i++ {

h ^= uint64(s[i])

h *= prime64

}

return h

}

哈希冲突

在哈希表中,哈希值相同的多个字符串可能会存储在同一个位置上,这种现象叫做哈希冲突。哈希冲突处理策略有开放寻址法、再哈希法和链地址法。

Go语言使用链地址法处理哈希冲突。对于每个存储单元,map结构体中还维护了一个[]keyValue类型的链表。

1

2

3

4

5

6

type hmap struct {

count int // 映射中的键值对数量

flags uint8 // 控制哈希表的一些属性

B uint8 // 用于计算哈希地址的初始大小

noverflow uint16 // 链表上的溢出桶的数量

}

Growing

在Go语言中,动态数组会自动地为map分配更多的空间。Growing过程涉及到将原始的数组重新复制到一个更大的数组中,其中原数组的元素需要重新计算其在新数组中的位置,而新数组的元素则需要将其键值对填充到相应的位置。Growing的过程比较复杂,可以由函数hashGrow()来控制。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// hashGrow() 将map的数组的大小翻倍,并处理哈希冲突。

func hashGrow(h *hmap) {

// ...

buf := make([]keyValue, newCap)

//...

for i := uintptr(0); i < cap; i++ {

// ...

evacuate(h, &h.oldbuckets[i], &buf)

// ...

}

// ...

}

// evacuate() 将一个bucket中的键值对重新映射到新的数组中

func evacuate(h *hmap, oldbuck *bucket, newbuck *[]keyValue) {

// ...

}

map扩容

双倍扩容

Go语言中的哈希表在map的数组容量达到一定程度时,就会自动进行扩容。扩容的依据是当前已存储的元素数量和数组的长度之间的比值:

Go语言的map会优先选择数组大小为原数组大小的2倍,以确保map在存储过程中有足够的空间存放新的元素。当元素数量达到85%时,Go语言就会再次对数组进行扩容,此时数组长度翻倍,以保证数组长度和元素数量的比例始终维持在0.75左右,以平衡效率和空间占用。

Growing过程

当映射中的元素数量超过85%时,Go语言就会触发map的扩容过程。在扩容的过程中,map会将原有的元素复制到新的数组中,并将新数组的初始大小设置为原数组的2倍。对于发生哈希冲突的元素,需要在新的数组中重新计算哈希地址。

避免溢出

当数组中元素的数量超过0x7fffffff(2^31-1,即int类型的最大值)时,就会发生溢出,此时数组的大小将无法达到原数组的2倍。所以Go语言会在初始创建map时,为其初始化一个较小的数组,并设置map的B值,以便在元素数量超过限制时再次进行扩容。当map中元素的数量超过阈值时,会再次翻倍,直到数组大小小于0x7fffffff为止。

代码分析

hashmap.go包含在Go语言源码中的src/container/map.go文件中。其中map结构体的定义和Growing实现都在runtime包中,在src/runtime/map.go文件中。

附录

原文链接:
相关文章
最新更新