Golang Map简介以及底层原理

 更新时间:2024年10月18日 10:46:28   作者:大广哥  
这篇文章主要介绍了Golang Map简介以及底层原理的相关资料,Go语言提供的map是一种键值对存储结构,支持基本操作如len、delete等,map是非线程安全的,可用sync.Mutex确保并发安全,为高效查找和插入,需要的朋友可以参考下

Map 简介

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

Map 定义

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()方法。

// 使用range循环遍历map中的所有键值对
for key, value := range map_var {
// TODO ...
}
// 计算map中的元素数量
if len(map_var) > 0 {
// TODO ...
}

Map 的线程安全

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

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哈希算法,算法代码如下:

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类型的链表。

type hmap struct {
count int // 映射中的键值对数量
flags uint8 // 控制哈希表的一些属性
B uint8 // 用于计算哈希地址的初始大小
noverflow uint16 // 链表上的溢出桶的数量
}

Growing

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

// 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的数组容量达到一定程度时,就会自动进行扩容。扩容的依据是当前已存储的元素数量和数组的长度之间的比值:

  • 当map的已存储元素数量小于map数组长度的一半时,元素的数量未达到哈希表效率的最大值,无需扩容;

  • 当map已存储的元素数量大于等于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文件中。

附录

  • 为什么哈希表的容量要设置为2的n次幂?为什么不是其他数字?

  • Go语言中的map是如何进行线程安全的?原理是什么?

  • map的数据结构是怎样的?如何实现键值对的查找、添加、删除操作?

  • 如何实现Growing过程?

  • 为什么map的扩容条件是85%,而不是100%?

  • 在go语言中如何创建map?

  • 为什么哈希冲突处理策略有开放寻址法、再哈希法和链地址法?

  • 如果存在冲突,键值对是如何存储在数组中的?

  • 为什么Growing过程中会创建一个较大的临时数组,而不是直接在原数组上扩展空间?

  • 如何实现map的迭代?

总结

本节我们学习了Go语言中的map数据类型,使用方法以及map的数据安全问题。

到此这篇关于Golang Map简介以及底层原理的文章就介绍到这了,更多相关Golang Map详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • go中如何获取本机ip地址

    go中如何获取本机ip地址

    这篇文章主要介绍了go中如何获取本机ip地址问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • 浅谈golang slice 切片原理

    浅谈golang slice 切片原理

    这篇文章主要介绍了浅谈golang slice 切片原理,详细的介绍了golang slice 切片的概念和原理,具有一定的参考价值,有兴趣的可以了解一下
    2017-11-11
  • go内存缓存如何new一个bigcache对象示例详解

    go内存缓存如何new一个bigcache对象示例详解

    这篇文章主要为大家介绍了go内存缓存如何new一个bigcache对象示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Golang Http 验证码示例实现

    Golang Http 验证码示例实现

    这篇文章主要介绍了Golang Http 验证码示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Windows下升级go版本过程详解

    Windows下升级go版本过程详解

    这篇文章主要为大家介绍了Windows下升级go版本过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 使用Golang搭建web服务的实现步骤

    使用Golang搭建web服务的实现步骤

    本文主要介绍了使用Golang搭建web服务的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • 深入理解Golang中的Protocol Buffers及其应用

    深入理解Golang中的Protocol Buffers及其应用

    本篇文章将深入探讨 Go 语言中使用 Protobuf 的基础知识、常见应用以及最佳实践,希望能帮大家了解如何在项目中高效利用 Protobuf
    2024-11-11
  • goland 清除所有的默认设置操作

    goland 清除所有的默认设置操作

    这篇文章主要介绍了goland 清除所有的默认设置操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Golang实现事务型内存数据库的方法详解

    Golang实现事务型内存数据库的方法详解

    内存数据库经我们经常用到,例如Redis,那么如何从零实现一个内存数据库呢,本文旨在介绍如何使用Golang编写一个KV内存数据库MossDB
    2023-03-03
  • Golang Heap的源码剖析

    Golang Heap的源码剖析

    这篇文章主要给大家详细剖析了Golang Heap源码,文中有详细的代码示例,对我们学习Golang Heap有一定的帮助,需要的朋友可以参考下
    2023-07-07

最新评论