golang集合map简介

本文详细介绍了Go语言中Map的使用,包括定义、长度、设置值、删除、清空、查询及遍历等操作。Map在底层通过哈希表实现,具有动态扩容机制。在查询和插入时,首先计算key的hash值,然后通过高位匹配bucket中的key。当负载因子超过6.5或溢出桶过多时,会发生扩容。同时,文章指出并发访问Map可能导致panic,建议避免并发操作。


map是一种无序的键值对集合。

map集合

map可通过key来快速地检索数据,且可以方便地对其进行迭代访问(其返回顺序是未知的)。

定义

通过make关键字可方便地构建map:

// 声明变量,为nil,不能对其进行数据操作(不能存放键值对;只能获取长度,或for-range)
var mpValue map[keyType]ValueType

// 使用make声明,可对其进行数据操作
mpValue := make(map[keyType]ValueType)

mpValue := make(map[keyType]ValueType, cap)

// 定义并初始化
mpTest := map[string]int{
	"second":2,
	"third":3,
}

make中传递的第二个元素是容量大小建议值hint,实际大小为能放下hint的最小 2 B 2^B 2B值;为避免扩容,hint需考虑一定余量(扩为8/6.5倍)。

后续示例以mpTest := make(map[string]int)为例。

长度

map可根据新增键值动态伸缩。虽然在定义map时可指定容量,但不能通过cap(mp)来获取其容量大小。

通过len(mp)来获取其内存放元素数量;若mp为nil,则返回0。

设置值

通过key赋值可添加元素:

  • 若key存在,则更新value;
  • 若key不存在,则添加键值对;
mpTest["first"] = 1
mpTest["first"] += 1

删除

通过内置delete函数来删除元素(参数为键key):

  • 若key存在,就删除对应的键值对;
  • 若key不存在,则无操作直接返回;

delete(mpTest, key)

清空

go中没有提供直接清空map的方法,可通过:

  • 删除map中的每一个元素,来清空;

    for k := range mpTest{
        delete(mpTest, k)
    }
    
  • 直接生成一个新的map;
    mpTest = make(map[string]int)

  • 若map不再使用,直接赋值为nil
    mpTest = nil

查询

查询与遍历时获取的v:

  • 若是指针类型结构,可直接修改v中字段(map中的值也会对应修改);
  • 否则,修改v,不影响map中值;

通过键可获取对应的值,并判断是否存在:

if v, ok := mpTest["first"]; ok{
    fmt.Println("Key found:", v)
}else{
	fmt.Println("Key not found")
}
// 若不需要对应的值,可
_, ok := mpTest["first"]

// 直接获取时,若对应键不存在,则返回对应类型的零值
v := mpTest["notExist"]

遍历

通过for可方便遍历:

for k, v := range mpTest{
	fmt.Println(k, "=", v)
}

for k := range mpTest{
	fmt.Println(k, "=", mpTest[k])
}

底层机制

map在底层是用哈希表实现的(hash数组+桶内的key-value数组+溢出的桶链表);hash表中的基本单位是桶,每个桶最多存8个键值对,超过了会链接到额外的溢出桶。hash表大小是2的指数。

所有的值会被Hash到不同的桶中:

struct Bucket
{
    uint8  tophash[BUCKETSIZE]; // hash值的高8位....低位从bucket的array定位到bucket
    Bucket *overflow;           // 溢出桶链表,如果有
    byte   data[1];             // BUCKETSIZE keys followed by BUCKETSIZE values
};

struct Hmap
{
    uint8   B;    // 可以容纳2^B个项
    uint16  bucketsize;   // 每个桶的大小

    byte    *buckets;     // 2^B个Buckets的数组
    byte    *oldbuckets;  // 旧的buckets,只有当正在扩容时才不为空
};

Bucket示意图:
Bucket
map底层创建时,会初始化一个hmap结构;并分配一个足够大的内存空间:前段用于hash数组;后段预留给溢出的桶。

整体结构示意图(各个桶在分配时是连在一起的):
hMAP+bucket

Bucket中key/value的放置顺序是:keys放在一起(在前),values放在一起(在后);这样可避免不必要的对齐操作处理。

key比较

在查询、插入时key需要先计算出hash值:

  • hash值的低位当作Hmap结构体中buckets数组的index,找到key所在的bucket;
  • hash值的高8位存储在了bucket的tophash中;在查找时,对tophash数组的每一项进行顺序匹配:
    • 先比较hash值高位与bucket的tophash[i]是否相等;若相等则再比较bucket的第i个的key与所给的key是否相等;若相等,则返回其对应的value;
    • 否则继续下一项比较(bucket完成后,还需在overflow buckets中继续);

bucket-key-compare

扩容

hmap扩容的负载因子是6.5,即count/( 2 B 2^B 2B)>6.5时就会发生扩容(扩容为原来两倍,即 2 B + 1 2^{B+1} 2B+1)。从旧数组将数据迁移到新数组时,不是一次全量拷贝完成的,而是在每次读写Map时以桶为单位做动态搬迁。从而避免了扩容时长时间的停顿。

在负载因子没有超标,但noverflow较多(B<=15时,noverflow>= 2 B 2^B 2B;B>15时,noverflow>= 2 15 2^{15} 215)的情况下,会发生等量扩容:创建和旧桶数目一样多的新桶,把旧桶中的数据放进去。发生这样的情况,一般是大量键值对被删除,通过等量扩容,可减少溢出桶的数量。

注意:map只会扩容,不会收缩;若map中删除元素太多,可通过申请新的map,把旧map复制过去的方式来收缩。

结论

在使用map时:

  • 带有容量的map初始化,可以有效的减少内存分配的次数,进而减少每次操作的耗时;
  • 每次扩容hash表增大1倍,hash表只增不减;
  • delete操作只置删除标志位,释放溢出桶的空间依靠触发等量扩容来实现;
  • map不支持并发。插入、删除、搬迁等操作会置writing标志,检测到并发直接panic;
  • 内置类型用值,构造的struct类型用指针;
  • 指针类型会影响golang.gc的速度;
### Golang 中 `map` 的数据结构实现 Go 语言中的 `map` 是一种哈希表的实现方式,其底层源码位于 Go 源代码目录下的 `src/runtime/map.go` 文件中[^1]。以下是关于 Go 地图类型的几个重要方面: #### 数据结构定义 在 Go 的运行时库中,`map` 类型的实际存储是由一个名为 `hmap` 的结构体来表示的。该结构体包含了管理键值对所需的各种字段,例如桶数组、溢出链表以及其他元数据。 ```go type hmap struct { count int // 键值对的数量 flags uint8 B uint8 // 表示桶数量的指数 log_2(len(bucket)) noverflow uint16 // 溢出桶的数量统计 hash0 uint32 // 哈希种子 buckets unsafe.Pointer // 指向第一个 bucket 数组 oldbuckets unsafe.Pointer // 如果正在进行扩容,则指向旧的 bucket 数组 nevacuate uintptr // 扩容过程中已迁移的桶索引 overflow *overflow // 溢出链表的第一个节点 } ``` 上述结构展示了 `hmap` 如何通过指针和位操作高效地管理和访问散列表的内容。 #### 存储机制 实际的数据是以桶(bucket)的形式存储的。每个桶可以容纳一定数量的键值对,默认情况下最多能存 8 对键值。如果某个桶满了而又有新的键值对加入时,会触发溢出处理逻辑,创建一个新的溢出桶并链接到原桶上形成链表结构。这种设计有助于减少冲突带来的性能损失。 当负载因子过高或者达到特定条件时,整个地图将会经历一次重新分配的过程——即所谓的扩容操作。在此期间,所有的现有条目会被均匀分布至新构建的一系列更大的桶集合里去[^5]。 #### 关于并发安全性的说明 需要注意的是,标准库提供的 `map` 并不是线程安全的;这意味着在同一时间如果有多个 goroutine 同时读写同一个映射对象的话可能会引发崩溃错误。因此,在多 Goroutines 环境下使用 map 应考虑同步控制措施,比如借助互斥锁 (`sync.Mutex`) 或者采用专门针对高并发场景优化过的容器类型如 `sync.Map` 来替代普通版本的地图实例[^3]。 ```go var m sync.Map m.Store("key", "value") // 插入/更新 key-value pair if val, ok := m.Load("key"); ok { fmt.Println(val.(string)) // 获取指定 key 的 value } else { fmt.Println("Key not found.") } ``` 以上就是有关 golang 内部如何具体实现 map 结构的一些基础知识概述以及简单例子展示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值