目录
使用 var 关键字进行声明,并使用 make 函数进行初始化
概述
本文主要是对map的总结,重点搜罗整理了哈希表相关知识,这个比较大篇幅地对哈希表的介绍做为理解map的铺垫,golang map的内部实现,map的相关操作,nil map和空map的区别,常用操作,使用过程中的注意点和常见panic,简单介绍了map的并发操作
一、哈希表原理
哈希表是一种常见的数据结构,用于实现键值对的存储和检索,golang和其他语言一样,也实现了哈希表,在介绍golang map之前,先了解一下哈希表基本语言里,对认识理解map有很大帮助。
哈希表(Hash Table)是一种常见的数据结构,用于实现键值对的存储和检索。其原理可以用一个类比的例子来说明:字典中收录了大量汉字的信息。为了便于快速找到某个字,可以首先创建一个按照每个字的拼音字母顺序排列的表(也就是字典开头部分的 “拼音检字表”),类似于在每个字和拼音字母之间建立了一种函数关系。要查找某个字时,只需在这个表中依次定位首字母、第二个字母、第三个字母…… 以此类推,大部分时候甚至不需要完整查找该字拼音的每个字母,就能确定这个字在字典中对应的准确位置。这个例子中,“查找拼音的第 n 的字母” 就是哈希函数的函数法则,而 “拼音检字表” 就可以理解为一种哈希表(或散列表)。
哈希函数
- 哈希表的核心是哈希函数,它将键映射到哈希表的存储位置。
- 哈希函数接受键作为输入,并生成一个固定长度的哈希值作为输出。
- 哈希函数的主要目的是让相同的输入内容始终产生相同的哈希值,而不同的输入内容则尽可能产生不同的哈希值
- 哈希值的范围通常比键的范围大得多,因此哈希函数能够将大量的键映射到有限的存储空间中。
哈希表和哈希函数的关系
- 哈希函数和哈希表之间存在密切关系。哈希函数是一种将输入数据映射到固定大小范围输出的函数,而哈希表则是利用哈希函数来实现高效数据存储和检索的数据结构。
- 哈希表通常由一个数组和一个哈希函数组成。当数据需要被插入哈希表时,哈希函数将数据映射为数组的索引,然后数据就被存储在该索引对应的位置。这个过程被称为哈希化。当需要检索数据时,哈希函数将再次应用于搜索键,以确定数据存储的位置。
- 哈希函数的设计对哈希表的性能至关重要。一个好的哈希函数能够产生均匀分布的哈希值,减少哈希冲突的概率。冲突是指两个或多个键被映射到相同的索引位置。为了处理冲突,哈希表通常采用开放寻址法或链表法等方法。
- 哈希函数是哈希表的关键组成部分,决定了数据在哈希表中的存储位置,而哈希表则通过有效地利用哈希函数实现了快速的数据检索和存储。
哈希表的优势
快速的查找、插入和删除:哈希表的设计使得对于给定的键,可以在常数时间内 (O(1)) 完成查找、插入和删除操作。这是因为哈希表通过哈希函数,可以直接计算出键对应的索引,而不需要遍历整个数据结构。
高效的内存利用:哈希表可以根据需要动态地调整大小,使其适应数据量的变化。这种动态调整的能力可以让哈希表确保内存得到高效利用。
适用于大规模数据:哈希表支持快速的检索、高效的插入和删除操作,以及良好设计的哈希函数带来的均匀散列,有效减少冲突的概率。
灵活性:哈希表适用于各种不同类型的数据和应用场景。哈希表可以存储键值对,适用于字典、集合等数据结构的实现。
解决冲突的办法:即使哈希表中发生哈希冲突(多个键映射到相同的索引),哈希表也有多种解决冲突的方法,如链地址法、开放地址法等。这种能力让哈希表能够在一定程度上减小冲突对性能的影响。
哈希冲突
什么是哈希冲突
由于哈希函数的输出范围远远小于键的范围,不同的键可能会映射到同一个桶中,两个或更多不同的输入数据被哈希函数映射到相同的哈希值或数组索引的情况。在哈希表中,哈希函数负责将数据映射到数组的特定位置,但由于输入空间远远大于输出空间,不同的输入可能会映射到相同的输出位置,引发冲突。
哈希冲突是在使用哈希表时常遇到的问题,因为哈希函数通常将输入空间映射到有限的输出空间,从而导致多个不同的输入可能映射到相同的哈希值。这种情况可能会影响哈希表的性能和正确性,因此需要使用冲突解决方法,如开放寻址法或链表法,来处理这种情况。
解决哈希冲突的目标是在不损失太多性能的情况下,确保哈希表中的数据项能够被正确地存储和检索。怎样选择适当的冲突解决方法取决于具体的应用场景和性能需求。
如何处理哈希冲突
链表法
链表法解决键值冲突的原理主要基于哈希表和链表的结合。具体来说,当两个不同的键经过哈希函数计算后得到相同的哈希值,即发生了冲突时,链表法采用以下步骤来解决这一问题:
- 哈希函数与桶的映射:首先,哈希表会根据哈希函数计算每个键的哈希值,并将这个哈希值映射到哈希表中的某个位置,这个位置通常被称为桶(bucket)。
- 链表存储:当两个或更多的键映射到同一个桶时,这些键值对不会互相覆盖,而是被组织成一个链表,并存储在这个桶中。链表中的每个节点都存储一个键值对,并通过指针连接到下一个节点。新插入的键值对会被添加到链表的尾部。
- 查询操作:当需要查询某个键对应的值时,哈希表会首先根据哈希函数找到对应的桶,然后遍历这个桶中的链表,直到找到与查询键相等的键为止。如果链表为空或遍历完整个链表都没有找到匹配的键,则表示该键不存在于哈希表中。
链表法的优点在于它结合了哈希表的快速访问特性和链表的动态修改特性。通过哈希函数,可以快速地定位到可能的键值对存储位置;而链表则提供了处理冲突的能力,使得多个键值对可以共享同一个桶。
然而,链表法也存在一些缺点。当冲突非常严重时,链表可能会变得非常长,导致数据的存储和检索效率下降。此外,链表的操作(如插入和删除)通常需要额外的指针操作,这可能会增加一些开销。
为了解决链表过长的问题,有些哈希表实现会采用其他策略,如开放地址法或再哈希法等。这些方法各有其优缺点,并适用于不同的应用场景。在实际应用中,选择哪种冲突解决方法取决于具体的需求和场景
开放寻址法
开发寻址法(Open Addressing)是哈希表中解决键值冲突的一种常见方法。与链表法(Chaining)不同,开发寻址法不将冲突的键值对存储在同一桶中的链表中,而是根据某种探测策略在哈希表的桶数组中查找一个可用的空位来存储键值对。
以下是开发寻址法解决键值冲突的基本原理:
哈希函数与初始位置:首先,哈希函数会根据键计算出一个初始的哈希值,该哈希值对应哈希表中的一个桶的位置。
探测冲突:如果初始位置已经被其他键值对占用(即发生了冲突),开发寻址法会使用一个探测函数(probing function)来寻找下一个可能的桶位置。探测函数可以是线性探测、平方探测、双重哈希等。
- 线性探测:线性探测是最简单的探测方法,它依次检查初始位置后的每个桶,直到找到一个空位。
- 平方探测:平方探测通过计算一系列平方数来探测不同的位置,旨在减少聚集现象(primary clustering)。
- 双重哈希:双重哈希使用第二个哈希函数来计算探测的步长,通常能得到更好的分布。
插入键值对:一旦找到了一个空桶,就将键值对插入到这个位置。
查询键值对:查询操作也从计算键的初始哈希值开始,然后按照相同的探测策略遍历桶,直到找到匹配的键或遍历完所有可能的桶。
删除键值对:在开发寻址法中,删除操作需要特别注意。简单地删除一个键值对可能会导致查询操作出现错误,因为探测策略可能会跳过被删除的空位。因此,删除操作通常需要标记空位而不是直接删除键值对,或者维护额外的数据结构来记录哪些位置是空的。
开发寻址法的优点包括:
- 无需额外的指针或链表节点,节省了空间。
- 缓存友好,因为数据在数组中连续存储,有利于局部性原理。
然而,它也有一些缺点:
- 加载因子(哈希表中元素数量与桶数量之比)较高时,冲突可能变得严重,导致查找效率降低。
- 删除操作可能更复杂,需要额外的处理。
总的来说,开发寻址法通过直接在哈希表的桶数组中查找空位来解决键值冲突,提供了一种不同于链表法的冲突解决策略。在实际应用中,选择链表法还是开发寻址法取决于具体的性能和空间需求,以及哈希表的使用场景。
哈希表常见操作过程
存储数据
- 哈希表通常是一个数组,每个位置称为一个桶(Bucket)。
- 当要存储一个键值对时,首先通过哈希函数计算键的哈希值,然后将键值对存储在哈希值对应的桶中。
检索数据
- 当要检索某个键对应的值时,首先通过哈希函数计算键的哈希值,然后定位到哈希值对应的桶。
- 如果使用链表法解决冲突,需要遍历链表来查找具有相同哈希值的键。
- 如果使用开放寻址法解决冲突,需要根据一定规则在哈希表中寻找存储键的位置
删除数据
- 当要删除某个键值对时,首先定位到键对应的桶,然后根据具体的解决冲突方法,找到键值对所在的位置并删除它。
总的来说,哈希表通过哈希函数将键映射到存储位置,并通过解决冲突的方法来处理不同键映射到同一个位置的情况,从而实现了快速的数据存储和检索。
常用的哈希算法
哈希算法可以将任意长度的二进制值映射为较短的固定长度的二进制值,这个固定长度的二进制值就是哈希值。哈希算法通常具有单向性,即不能通过哈希值反向推导出原始数据。此外,哈希算法对输入数据非常敏感,即使原始数据只修改了一个比特,最后得到的哈希值也会大不相同。
哈希算法主要解决以下问题:
- 数据完整性校验:通过计算数据的哈希值,可以验证数据在传输或存储过程中是否被篡改。
- 数据加密:哈希算法可用于密码存储,由于哈希的单向性,即使哈希值被泄露,攻击者也无法还原出原始密码。
- 数据去重:通过比较哈希值,可以快速判断两个数据是否相同。
接下来,我将分别解释MD4、MD5、SHA-1和SHA-256的优缺点及适用场景:
- MD4:
- 优点:计算速度较快,适用于对性能有一定要求的场景。
- 缺点:已知存在一些碰撞攻击,安全性较低,不建议用于需要高安全性的场景。
- 适用场景:由于安全性问题,MD4目前已较少使用。
- MD5:
- 优点:广泛应用,计算速度适中,适用于多种场景。
- 缺点:已知存在多种碰撞攻击,安全性已受到质疑,不再适用于需要高度安全性的场景。
- 适用场景:在一些对安全性要求不高的场景下,如非敏感数据的完整性校验,MD5仍可使用。但请注意,对于密码存储等需要高安全性的场景,应使用更安全的哈希算法。
- SHA-1:
- 优点:相较于MD5,SHA-1的安全性稍高,适用于一些对安全性有一定要求的场景。
- 缺点:随着计算机技术的发展,SHA-1的安全性也逐渐受到威胁,已知存在实际碰撞攻击的例子。因此,SHA-1已不再被视为安全的哈希算法。
- 适用场景:由于安全性问题,SHA-1已逐渐被淘汰,不建议用于新的安全敏感应用。但在一些旧有系统中,可能仍会看到SHA-1的使用。
- SHA-256:
- 优点:安全性高,碰撞攻击的难度极大,适用于需要高度安全性的场景。此外,SHA-256还具有雪崩效应,即输入数据的微小变化会导致哈希值的显著变化,这有助于确保数据的完整性。
- 缺点:相较于MD5和SHA-1,SHA-256的计算速度可能稍慢一些。
- 适用场景:SHA-256广泛应用于密码学、数字签名、数据加密等领域,特别是在需要高度安全性的场景中,如密码存储、重要数据的完整性校验等。
哈希表的应用场景
- 字典和关联数组: 哈希表常被用作字典或关联数组的实现,其中键和值之间的映射关系可以通过哈希表快速查找。
- 数据库索引:数据库系统使用哈希表来实现索引,以加速对数据库表的查找操作,特别是在查找键值对的情况下。
- 缓存实现:由于哈希表提供快速的查找和插入操作,它经常被用于实现缓存系统,以加速对先前访问过的数据的访问。
- 唯一性检查:在需要保持唯一性的数据集中,哈希表可以用于检查新元素是否已存在,以避免重复。
- 文件系统和哈希表索引: 文件系统中的文件名到文件路径的映射,以及文件块到磁盘上的位置的映射,通常使用哈希表来实现。
- 分布式系统中的一致性哈希: 一致性哈希算法通过哈希函数将数据映射到节点,能够在节点的增减时,尽可能减少哈希表中的数据重新分布。这在分布式系统中的负载均衡和数据分布方面十分有用。
- 密码学和数字签名:哈希表在密码学中广泛应用,例如在数字签名中,用于生成消息摘要。
- 分布式缓存:在分布式系统中,哈希表常被用于实现分布式缓存,以加速对远程数据的访问。
- 数字签名:哈希算法在数字签名中有重要应用,它可防止数据被伪造、篡改,同时可以实现数据的多重加密和客户的身份认证。数字签名可以作为身份认证的依据,也可以作为签名者签名操作的证据。
二、golang map
在Go语言中,map
是一种内建的数据结构,一种无序的键值对集合,其中的键是唯一的,用于快速查找值。map是引用类型,意味着map作为函数参数时调用时,对map内容的修改,对调用者是可见的。https://go.dev/doc/effective_go#maps
map的内部结构
前面之所以拿词典来类比理解哈希表,是因为词典和哈希表在数据结构上有着相似之处,它们都是用于存储键值对的数据结构,通过键来快速查找值。在词典中,每个单词(键)都有一个与之对应的解释或定义(值)。同样,在哈希表中,每个键都映射到一个值。这种映射关系使得它们都能以接近O(1)的时间复杂度进行查找操作。
在 Go 语言的 map
数据结构中,hmap
和 bmap
是内部使用的结构,用于实现哈希表。这些结构是 Go 运行时库的一部分,而不是公开暴露给程序员的接口,所以通常不需要直接与之交互。然而了解它们的基本概念和他们之前的联系作用可以帮助你更好地理解 map
的工作原理,golang map的底层实现不同的版本略有差别,这里不对其内部实现细节做介绍,只对其大致的实现过程做了介绍,有来总体的认识,其中最重要的三个结构是,hmap、bucket、bmap,他们之前的关系大致如图示:
现在,我们来看哈希表在Go语言中的实现,特别是关于hmap、bmap和bucket的概念和联系:
hmap
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
在Go语言的
map
实现中,hmap代表了哈希表的总体结构。它包含了哈希表的元数据,如哈希函数、桶(bucket)数组、大小、扩容阈值等。hmap的作用是管理整个哈希表,包括确定键值对应该放在哪个桶中,以及处理哈希冲突等情况.
bmap
// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
- bmap是哈希表中的一个关键组件,它代表了一个具体的桶(bucket)。在Go中,每个桶都可以存储一定数量的键值对。当向哈希表中插入一个新的键值对时,hmap会根据键的哈希值确定它应该放在哪个桶中,然后在这个桶的bmap中进行存储。
- 如果一个桶的bmap已经满了,就需要处理哈希冲突。Go中的实现可能采用链表法来解决冲突,即在bmap内部使用一个链表来存储具有相同哈希值的键值对。
bucket(桶)
- 桶是哈希表中的一个固定大小的存储单元,用于存储键值对。在Go的哈希表实现中,桶是通过数组来组织的,每个数组元素指向一个bmap。这种结构使得哈希表能够高效地根据键的哈希值定位到对应的桶,进而找到或存储键值对。
- 桶的数量(即数组的长度)在哈希表的创建时确定,并且随着哈希表的增长而动态调整。当哈希表中的元素数量达到一定阈值时,哈希表会进行扩容,创建更多的桶来分散键值对,以减少哈希冲突并提高性能。
hmap bmap bucket之间的联系
- hmap是哈希表的整体结构,它负责管理和维护整个哈希表的状态。
- bmap是哈希表中的具体存储单元,它代表了一个桶,并存储了键值对。
- 桶是哈希表的基本存储单位,通过数组组织起来,用于根据键的哈希值定位到具体的存储位置。
总的来说,hmap、bmap和bucket共同构成了Go语言中哈希表的基本结构和工作原理。hmap管理整个哈希表,bmap代表具体的存储单元,而桶则是哈希表的基本存储单位。它们协同工作,使得哈希表能够高效地存储和检索键值对。
map的定义
map的定义方式为map[keyType]valueType,其中键(keyType)和值(valueType)都有着约束条件和数据类型的限制.
键的约束
唯一性
每个键必须是唯一的,即相同的键只能出现一次。这意味着在
map
中不允许存在重复的键,每个键都对应着一个唯一的值。如果尝试向map
中添加已经存在的键,则会覆盖原有的值,而不会创建新的键值对。
例如:
m := make(map[string]int)
m["apple"] = 1
m["banana"] = 2
m["apple"] = 3 // 覆盖了之前的值
可比较性
可比较性:键必须是可比较的类型。可比较的类型是指支持相等性比较“=”和大小比较的类型。这是因为map的实现依赖于键的哈希值来确定存储位置,而哈希值的计算需要基于键的比较操作。如果键是不可比较的类型,则无法计算其哈希值,也就无法在map中使用。
在Go语言中,可比较性是一种重要的概念,它决定了哪些数据类型可以进行相等性和大小比较。如果数据类型支持相等性和大小比较,则称该类型是可比较的。
在Go语言中,以下数据类型是可比较的,因此可以作为
map
的键值:
- 所有的基本类型(包括整数、浮点数、复数、布尔类型等)。
- 字符串类型(
string
)。- 数组类型(数组的元素类型必须也是可比较的)。
- 结构体类型(结构体的字段类型必须全是可比较的)。
- 指针类型(指针指向的数据类型必须也是可比较的)。
- 接口类型(接口的动态类型必须也是可比较的)。
示例:
package main
import "fmt"
func main() {
// 整数类型
m1 := map[int]string{1: "one", 2: "two"}
// 字符串类型
m2 := map[string]int{"apple": 1, "banana": 2}
// 指针类型
x := 42
m3 := map[*int]string{&x: "forty-two"}
// 数组类型
m4 := map[[3]int]string{[3]int{1, 2, 3}: "one two three"}
// 结构体类型
type Person struct {
Name string
Age int
}
m5 := map[Person]int{
{Name: "Alice", Age: 30}: 1,
{Name: "Bob", Age: 35}: 2,
}
fmt.Println(m1, m2, m3, m4, m5)
}
运行结果:
注:这里需要注意的是结构体类型作为键的情况,只有结构体的所有字段是可比较类型的,那么结构体才可以作为键值。结构体里有一个字段不是可比较类型,那么会抛出
invalid map key
例如下面的示例,切片作为结构体字段时,结构体是不可以作为键的。
package main
import "fmt"
type Person struct {
Name string
Age []int // 切片类型不是可比较的
}
func main() {
// 尝试创建一个结构体切片
p1 := Person{Name: "Alice", Age: []int{30}}
p2 := Person{Name: "Bob", Age: []int{35}}
// 尝试使用结构体作为map的键类型
m := map[Person]int{
p1: 1,
p2: 2,
}
fmt.Println(m)
}
运行结果:
map的相关操作
map的声明初始化
使用 make
函数进行声明和初始化
m := make(map[keyType]valueType)
这种方式创建了一个空的map,可以在后续的代码中动态添加键值对。推荐方式
使用字面量进行声明和初始化
m := map[keyType]valueType{ key1: value1, key2: value2, // 更多键值对... }
这种方式可以在声明map的同时初始化一些键值对。
使用 var
关键字进行声明,并使用 make
函数进行初始化
var m map[keyType]valueType m = make(map[keyType]valueType)
这种方式先声明了一个map变量,然后通过
make
函数初始化。
使用 var
关键字声明一个空的map
var m map[keyType]valueType
这种方式只声明了一个map变量,但不会进行初始化,map的值为
nil
,需要后续通过make
函数进行初始化后才能使用。不推荐使用这种方式
nil map和空map
nil map
- nil map是指未初始化的map变量,或者是已经被显式赋值为nil的map变量。
- nil map不能直接赋值,不能用来存储键值对,否则会导致运行时panic。
- nil map在使用前必须使用make函数进行初始化。
var m map[string]int
fmt.Println(m == nil) // 输出: true
m = make(map[string]int)
fmt.Println(m == nil) // 输出: false
m = nil
fmt.Println(m == nil) // 输出: true
空map
- 空map是指已经声明但没有初始化的map变量。
- 空map不包含任何键值对,但是它是一个有效的map变量,可以使用。
- 当尝试访问空map的元素时,会返回对应值类型的零值。
var m map[string]int
fmt.Println(m == nil) // 输出: true
对未初始化map操作
读取map中的元素: 当尝试从未初始化的map中读取元素时,会返回元素类型的零值,而不会导致panic。
删除map中的元素: 如果尝试从未初始化的map中删除元素,不会产生任何效果,也不会导致panic。
for range遍历map中的元素:遍历未初始化的map,不产生任何效果,也不会产生panic,即使在循环体内写入元素也不会panic,因为根本不会进入循环体。
写入map中的元素: 当尝试向未初始化的map中写入元素时,会引发运行时panic,因为未初始化的map的值为nil,无法写入元素。
使用len函数获取map的长度: 如果尝试使用len函数获取未初始化的map的长度,会返回0,而不会导致panic
总的来说,对未初始化的map读操作,不会产生panic,而是返回0,或者不产生任何效果。
对未初始化的map写操作,则会产生panic
注意事项
- 当需要检查一个map是否被初始化时,应该使用
m == nil
来检查,而不是len(m) == 0
,因为空map同样是len(m)==0。m := make(map[string]int) fmt.Println(m == nil) // false,是空map,不是nil map var m1 map[string]int fmt.Println(m1 == nil) //true,是nil map
- 尽量避免使用nil map,应该尽早初始化map,并尽量保持map处于初始化状态。
- 最好使用用内置的函数make来m := make(map[keyType]valueType)来创建和初始化。
var m map[string]int m["key"] = 1 //panic: assignment to entry in nil map
- 最好避免直接用var只声明map而不对其初始化。
m := make(map[string]int) m["key"] = 1 // 写入元素 fmt.Println(m) //map[key:1]
添加键值对
m := make(map[keyType]valueType)
m[key1] = value1
删除键值对
delete(m, key)
更新键值对
m[key] = newValue
查询键值对(用键访问值)
value := m[key]
检查键是否存在
value, ok := m[key]
if ok {
fmt.Println("键存在,值为:", value)
} else {
fmt.Println("键不存在")
}
遍历map
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
注意
for range
循环遍历map时,每次迭代的键值对的顺序并不是固定的。因为map是一种无序数据结构,map的内部实现采用了哈希表,它会根据键的哈希值将键值对分散存储在内存中的不同位置,而不是按照插入顺序或者其他顺序存储。因此,当遍历map时,迭代的顺序可能会受到存储位置、哈希算法等因素的影响,导致结果顺序不一致。
清空map(情况所有键值对)
置为nil
map是引用类型,可以直接置为nil清空,但是这种方式有一种弊端是在重新赋值就会panic。(不推荐)
m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m = nil // 清空map
m["k1"] = 6 //panic: assignment to entry in nil map
make函数重新分配内存
m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
fmt.Println(m) //map[k1:7 k2:13]
m = make(map[string]int)
fmt.Println(m) //map[]
clear清空
清空但不置为nil,推荐使用
m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
clear(m)
fmt.Println(m) //map[]
map的并发操作
对包括map在内的引用类型(如map、切片、通道等)的写入,如果没有适当的同步措施,可能会导致数据竞争和数据不一致的问题,最终可能导致程序panic。这种情况并不局限于map,其他引用类型也存在类似的问题。
不使用同步机制
示例:
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[int]int)
for i := 0; i < 1000; i++ {
go func() {
m[0]++ // 并发写入
}()
}
time.Sleep(time.Second) // 等待goroutine执行完毕
fmt.Println("Map length:", len(m))
}
运行结果:
使用sync.Mutex
示例:
package main
import (
"fmt"
"sync"
)
func main() {
var m = make(map[int]int)
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
m[0]++ // 使用Mutex加锁保护并发写入
}()
}
wg.Wait()
fmt.Println("Map length:", len(m))
}
运行结果:
使用sync.Map
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
v, _ := m.LoadOrStore(0, 0) // 使用LoadOrStore操作
m.Store(0, v.(int)+1) // 并发写入
}()
}
wg.Wait()
m.Range(func(key, value interface{}) bool {
fmt.Printf("Key: %v, Value: %v\n", key, value)
return true
})
}
运行结果:
关于sync.Map的简单介绍,Go 语言原生 map 并不是线程安全的,对它进行并发读写操作的时候,需要加锁。而 sync.map
则是一种并发安全的 map,在sync.Map
是 Go 语言提供的一种并发安全的 Map 实现,它可以在并发环境中安全地读取和写入数据,而无需额外的锁操作, Go 1.9 引入。关于它的介绍,此篇不做介绍,待时间,另起专篇。
小结
在 Go 语言中,map 是非线程安全的数据结构,这意味着在并发环境下对同一个 map 进行读写操作可能会导致数据竞态和内部数据结构的破坏,最终可能导致程序崩溃(panic)。导致并发操作 map 会产生 panic 的主要原因有以下几点:
数据竞态: 在并发环境中,多个 goroutine 同时对同一个 map 进行读写操作时,可能会导致数据竞态。比如一个 goroutine 正在对 map 进行写操作,而另一个 goroutine 此时尝试对同一个 map 进行读操作,这样就会出现竞争条件,可能导致 map 内部数据结构的破坏,最终导致 panic。
不一致的状态: 并发操作 map 还可能导致 map 内部数据结构处于不一致的状态。在并发环境中,多个 goroutine 同时对 map 进行读写操作,可能会破坏 map 的内部一致性,导致 map 处于不正确的状态,最终导致 panic。
Map 扩容: 在 map 扩容时,会涉及到对旧桶中的元素重新分配到新的桶中。如果在这个过程中有其他 goroutine 同时对 map 进行读写操作,可能会导致数据结构的混乱,进而触发 panic。
为了避免在并发环境中对 map 进行操作时发生 panic,可以采取以下措施:
- 使用互斥锁(sync.Mutex)或读写锁(sync.RWMutex)来保护 map 的读写操作,确保同一时间只有一个 goroutine 对 map 进行写操作。
- 使用并发安全的数据结构,如 sync.Map,它内部实现了并发安全的读写操作,不需要额外的同步操作。
- 尽量避免在并发环境中对同一个 map 进行读写操作,可以考虑通过通信来避免共享状态,或者对 map 进行复制以避免竞态条件