哈希表和数组是最常见的数据结构,几乎所有的语言都会有数组和哈希表两种容器类型 。哈希表表示的是键值对之间映射关系,在Go语言中,通过map
来表示哈希表。本文将深入浅出介绍map
的概念、使用方式、底层结构、性能、最佳实现等话题,帮助开发更好的理解和使用map
。
map
的基本概念和使用map
在Go语言中,map
是一种内置的数据结构,用于存储键值对。Go语言中的map
有如下特点
map
是Go语言内置的数据结构,它是一种无序的键值对集合,其中键是唯一的。Go语言在语言级别支持map
, 使用方便。map
提供了非常快速的查找、插入和删除操作,这些操作的平均时间复杂度为O(1)
。这使得map
非常适合用于需要快速访问数据的场景。map
是动态的,可以在运行时动态地增加或删除键值对,而不需要预先声明大小。map
的定义和初始化初始化Map有几种方式:
// 方式一: 使用var关键字声明Map,然后使用make函数初始化
var myMap map[string]int
myMap = make(map[string]int)
// 方式二: 使用make函数直接声明并初始化Map
myMap := make(map[string]int)
// 方式三: 使用Map字面量初始化Map,这在创建预填充的Map时非常有用
myMap := map[string]int{
"apple": 5,
"banana": 10,
}
注意:使用Map时,如果没有初始化(即值为nil
),直接赋值会导致运行时错误。
map
基本操作:增、删、改、查下面是对map
进行增、删、改、查的基本方法
// 增(Insert): 向Map中添加新的键值对; 如果key已存在,则更新value
myMap["orange"] = 15
// 删(Delete): 从Map中删除键值对; 如果key不存在,delete函数不会执行任何操作。
delete(myMap, "apple")
// 改(Update): 更新Map中的键值对; 如果key已经存在,这将替换原来的值。如果key不存在,这将添加一个新的键值对
myMap["banana"] = 20
// 查(Lookup):查找Map中的键对应的值;
value, exists := myMap["banana"] // 这里value是与键关联的值,exists是一个布尔值,如果键存在于Map中,则为true;如果键不存在,则为false,并且value将是类型的零值。
value := myMap["banana"] // 如果你只关心值,可以忽略第二个返回值
注意:map
在并发环境下不是线程安全的,如果需要在多个goroutine中使用Map,应该使用互斥锁(sync.Mutex
)或者使用sync.Map
。
在Go语言中,可以使用for
循环和range
关键字来遍历Map。遍历时,range
表达式返回两个值:键和对应的值。这里是一个基本的例子:
myMap := map[string]int{
"apple": 5,
"banana": 10,
"cherry": 15,
}
for key, value := range myMap {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
在上面的代码中,key
变量会遍历Map中的所有键,而value
变量会遍历对应的值。每次迭代时,key
和value
都会更新为当前遍历到的键值对。
可以忽略第二个变量
for key := range myMap {
如果你只需要值,可以使用下划线_
来忽略键:
for _, value := range myMap {
**需要注意的是,Map的遍历顺序是随机的,每次遍历的顺序可能都不一样。**这是因为Go的Map类型是故意设计为无序的,以避免依赖特定的遍历顺序,这可能会导致程序中的一些隐蔽的bug。
此外,虽然可以在遍历过程中删除或修改Map中的键值对,但是添加键值对可能会导致未定义的行为。如果需要在遍历时修改Map,建议先记录需要做出的更改,然后在遍历结束后进行。
在Go语言中,map
本质上是哈希表(hash table)。哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。实现哈希表的两个关键是哈希函数和解决哈希冲突。
哈希函数,也被称为散列函数,是一种将任意长度的输入(如字符串)通过特定的散列算法,变换成固定长度的输出(即哈希值或消息摘要)的函数。通常,会使用类似数组(连续内存)的形式来存储哈希值,从而保证哈希值的访问性能。
哈希函数应该尽可能保证不同的输入有相同的输出。
如上图所示,哈希函数的结果分布较为均匀,哈希值的增删改查的时间复杂度为O(1)
在实际场景中,因为可能的输入范围通常是远超映射范围(输出的范围,受存储空间的影响)。因而难免会出现不同的输入得到相同的输出,即发生哈希冲突。
大多数的编程语言都用拉链法实现哈希表, 拉链法通常使用数组和链表作为底层数据结构。
哈希值使用数组将哈希值HashValue相同的Key对应的Value通过链表数组进行维护
哈希函数将哈希键Key
映射到数组的索引,数组的每一个元素都有一个Value
桶,使用链表进行维护。
在Go语言中,map使用类似拉链法的方式实现哈希表,Go语言运行时同时使用了多个数据结构组合表示哈希表。
// runtime/map.go
// A header for a Go map.
type hmap struct {
count int // 当前哈希表中的元素数量
flags uint8
B uint8 // 当前哈希表持有的 buckets 数量, 因为哈希表中桶的数量都按2倍扩容,改字段存储对数,也就是 len(buckets) == 2^B
noverflow uint16 // 溢出桶的大致数量
hash0 uint32 // hash seed
buckets unsafe.Pointer // 存储 2^B 个桶的数组
oldbuckets unsafe.Pointer // 扩容时用于保存之前 buckets 的字段 , 大小事buckets的一般
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
// mapextra 主要维护,当hmap中的buckets中有一些桶发生溢出,但有达不到扩容阈值时,存储溢出的桶
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
在runtime.hmap
结构体中,buckets
字段是一个unsafe.Pointer
, 因为go语言中支持不同类型的键值对,需要在编译时才能确定map
的类型。
可以查看编译时如何重建hmap类型reflectdata.MapType()
func MapType(t *types.Type) *types.Type {
if t.MapType().Hmap != nil {
return t.MapType().Hmap
}
bmap := MapBucketType(t) // 构建bmap类型
fields := []*types.Field{
makefield("count", types.Types[types.TINT]),
makefield("flags", types.Types[types.TUINT8]),
makefield("B", types.Types[types.TUINT8]),
makefield("noverflow", types.Types[types.TUINT16]),
makefield("hash0", types.Types[types.TUINT32]), // Used in walk.go for OMAKEMAP.
makefield("buckets", types.NewPtr(bmap)), // Used in walk.go for OMAKEMAP.
makefield("oldbuckets", types.NewPtr(bmap)),
makefield("nevacuate", types.Types[types.TUINTPTR]),
makefield("extra", types.Types[types.TUNSAFEPTR]),
}
hmap := types.NewStruct(types.NoPkg, fields)
hmap.SetNoalg(true)
types.CalcSize(hmap)
// The size of hmap should be 48 bytes on 64 bit
// and 28 bytes on 32 bit platforms.
if size := int64(8 + 5*types.PtrSize); hmap.Size() != size {
base.Fatalf("hmap size not correct: got %d, want %d", hmap.Size(), size)
}
t.MapType().Hmap = hmap
hmap.StructType().Map = t
return hmap
}
这里可以看出buckets
是指向bmap
的指针, bmap
也是在编译时通过bmap := MapBucketType(t)
进行构建的,而非runtime.bmap
中的定义那般简单:
// runtime.bmap
type bmap struct {
tophash [bucketCnt]uint8
}
可以查看
// MapBucketType makes the map bucket type given the type of the map.
func MapBucketType(t *types.Type) *types.Type {
if t.MapType().Bucket != nil {
return t.MapType().Bucket
}
keytype := t.Key()
elemtype := t.Elem()
types.CalcSize(keytype)
types.CalcSize(elemtype)
if keytype.Size() > MAXKEYSIZE {
keytype = types.NewPtr(keytype)
}
if elemtype.Size() > MAXELEMSIZE {
elemtype = types.NewPtr(elemtype)
}
field := make([]*types.Field, 0, 5)
// The first field is: uint8 topbits[BUCKETSIZE].
arr := types.NewArray(types.Types[types.TUINT8], BUCKETSIZE)
field = append(field, makefield("topbits", arr))
arr = types.NewArray(keytype, BUCKETSIZE)
arr.SetNoalg(true)
keys := makefield("keys", arr)
field = append(field, keys)
arr = types.NewArray(elemtype, BUCKETSIZE)
arr.SetNoalg(true)
elems := makefield("elems", arr)
field = append(field, elems)
// If keys and elems have no pointers, the map implementation
// can keep a list of overflow pointers on the side so that
// buckets can be marked as having no pointers.
// Arrange for the bucket to have no pointers by changing
// the type of the overflow field to uintptr in this case.
// See comment on hmap.overflow in runtime/map.go.
otyp := types.Types[types.TUNSAFEPTR]
if !elemtype.HasPointers() && !keytype.HasPointers() {
otyp = types.Types[types.TUINTPTR]
}
overflow := makefield("overflow", otyp)
field = append(field, overflow)
// link up fields
bucket := types.NewStruct(types.NoPkg, field[:])
bucket.SetNoalg(true)
types.CalcSize(bucket)
// ......
return bucket
}
重建后的结构体如下所示:
type bmap struct {
topbits [8]uint8
keys [8]keytype
elems [8]elemtype
overflow uintptr
}
综上所示,map
的底层结构如下图所示:
map
的操作性能分析Map的空间复杂度取决于存储的键值对数量以及哈希桶的数量。在Go中,Map的空间复杂度通常可以认为是O(n),其中n是键值对的数量。随着键值对数量的增加,Map可能会进行扩容,增加哈希桶的数量,以保持操作的效率。
myMap := make(map[string]int, initialCapacity)
int
类型通常比string
类型作为键更高效。sync.Map
。sync.Map
提供了一些优化,不需要开发者自己实现同步,可以在并发环境中提供更好的性能。map
的扩容策略在Go语言中,Map的扩容是一个自动的过程,旨在维护Map的性能,特别是插入和查找操作的时间复杂度。
map在写入哈希键值对时runtime.mapassign, 会判断是否需要扩容:
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
...
}
// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
return noverflow >= uint16(1)<<(B&15)
}
可以看出扩容的两个条件:
overLoadFactor(h.count+1, h.B)
, 装载因子:=元素数量÷桶数量tooManyOverflowBuckets(h.noverflow, h.B))
h.growing()
用于避免并发扩容,导致混乱。
当Map需要扩容时,Go运行时会进行以下步骤:
func hashGrow(t *maptype, h *hmap) {
...
// 原有桶设置给oldbuckets
oldbuckets := h.buckets
// 创建新桶
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// commit the grow (atomic wrt gc)
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
...
}
键值对的迁移并摆在hashGrow中进行,而是在runtime.mapassign中逐步迁移的。
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
if h.growing() {
growWork(t, h, bucket)
}
...
}
runtime.growWork中执行迁移的具体工作
func growWork(t *maptype, h *hmap, bucket uintptr) {
// make sure we evacuate the oldbucket corresponding
// to the bucket we're about to use
evacuate(t, h, bucket&h.oldbucketmask())
// evacuate one more oldbucket to make progress on growing
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
扩容是一个昂贵的操作,它涉及到内存分配和键值对的重新哈希。在扩容期间,Map的性能可能会暂时下降,特别是在插入新元素时。然而,扩容完成后,由于减少了哈希冲突,Map的性能通常会得到提升。
map
与并发安全在Go语言中,标准的Map不是并发安全的。这意味着如果多个goroutine同时对同一个Map进行读写操作,可能会导致竞态条件(race condition),从而引发不可预知的行为,如Map的内部结构损坏、程序崩溃等。
并发问题主要发生在以下情况:
为了避免这些问题,需要采取措施来确保对Map的并发访问是安全的。
sync.Map
sync.Map
是Go语言在sync
包中提供的一个并发安全的Map类型。它使用了一些优化技术来减少锁的粒度,提高并发性能。
sync.Map
提供了以下几个主要的方法:
Store(key, value)
:存储键值对。Load(key)
:根据键获取值。LoadOrStore(key, value)
:获取或存储键值对。Delete(key)
:删除键值对。Range(f func(key, value interface{}) bool)
:遍历Map。sync.Map
适用于以下使用场景:
sync.Map
在这种场景下性能较好,因为它减少了锁的竞争。sync.Map
可以有效地减少锁的粒度,提高并发访问的性能。sync.Map
可以在不加锁的情况下安全地返回同一个键的值。关于sync.Map
的更多介绍,参考《深入理解Go语言sync.Map》
Map适用于以下场景:
make(map[KeyType]ValueType, size)
预分配足够的空间可以减少扩容带来的性能损耗。int
或int64
,可以减少哈希计算的开销。sync.Map
或自行实现的并发安全Map,确保使用适当的锁策略。通过遵循这些最佳实践和技巧,可以有效地使用Map,并优化其性能。在实际开发中,应该根据具体的应用场景和需求来选择和调整策略。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有