Golang map实现原理及源码分析

本文涉及到的源码版本为Go SDK 1.16.1 

1、map的基本结构

        map是Golang中的一种常用数据结构,其本质上是一种哈希表,类似于 java 的 HashMap以及Python的字典(dict),是一种存储键值对(Key-Value)的数据结构。一般的Map会包含两个主要结构:

  • 数组:数组里的值指向一个链表
  • 链表:目的解决hash冲突的问题,并存放键值

        而在Golang中,解决hash冲突的不是链表,而是数组(既内存中的连续空间),而且使用了两个数组分别存放键和值,如下图所示。其中提到了map中的两个核心的结构体:hmap和bmap,每个map的底层数据结构是hmap,而每个hamp是有若干个bmap组成的bucker数组hmap的结构体定义如下(src/runtime/map.go): 

type hmap struct {
	count     int //元素个数
	flags     uint8
	B         uint8  // 扩容相关字段, B是buckets数组的长度的对数2^B
	noverflow uint16 // 溢出的bucket个数,一个bmap中最多存8组键值对,存满了就往指向的这个bmap里存
	hash0     uint32 // hash因子

	buckets    unsafe.Pointer // buckets 数组指针
	oldbuckets unsafe.Pointer // 扩容时,存放之前的buckets(Map扩容相关字段)
	nevacuate  uintptr        // 分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段)

	extra *mapextra // 用于扩容的指针
}

type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}

  而bmap 就是我们常说的“桶”,桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是相同的。哈希函数相同的情况下又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。 bmap在map.go中的代码定义如下,值得一提的的是,在编译时会动态创建一个新的bmap结构:

// A bucket for a Go map.
type bmap struct {
	tophash [bucketCnt]uint8 //长度为8的数组
}

//编译时创建的新bamp结构
type bmap struct {
  topbits  [8]uint8 //长度为8的数组,[]uint8,存储的是key获取的hash的高8位,遍历时对比使用,提高性能。
  keys     [8]keytype //长度为8的数组,存储具体的key值。
  values   [8]valuetype //长度为8的数组,存储具体的value值
  pad      uintptr //指向的hmap.extra.overflow溢出桶里的bmap,上面的字段topbits、keys、elems长度为8,最多存8组键值对,存满了就往指向的这个bmap里存
  overflow uintptr //对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件
}

       在了解了hmapbmap的基本结构后,就可以就得到如下的Map结构图:

  

 2、负载因子

        负载因子是衡量Hash表中空间利用率的指标,负载因子= hash表中已存储的键值对的总数量/hash桶的个数(即hmap结构中buckets数组的个数)。我们知道负载因子决定了Hash表的存储效率:负载因子过小会导致空间利用率较低;而负载因子较大则会导致Hash表的插入和查找频繁发生碰撞,导致Hash表退化成链表,触发扩容。在Golang中负载因子被设定为6.5,为什么设置为6.5而不是其他的数值呢,Golang官方给出了一组测试数据:

        从表中我们能够看到当负载因子设置为6.5时,溢出率(%overflow),每个键值对占用字节比(bytes/entry)和查找次数取得一定平衡 。当在Golang中调用make(map[k]v,hint)初始化map时,会根据初始化元素个数以及负载因子来决定创建的buckets个数,核心代码如下所示:

func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang 中,中间件是一种常见的设计模式,用于在请求处理过程中添加公共功能、处理请求前后的逻辑等。下面是一种常见的 Golang 中间件实现原理: 1. 定义一个中间件函数类型,它接受一个 `http.Handler` 参数,并返回一个新的 `http.Handler` 对象。例如: ```go type MiddlewareFunc func(http.Handler) http.Handler ``` 2. 编写一个具体的中间件函数,它符合上述定义的中间件函数类型。该函数通常会包装原始的 `http.Handler` 对象,添加额外的逻辑或修改请求/响应。 ```go func LoggerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 添加日志记录逻辑 log.Println("Handling request:", r.URL.Path) // 调用下一个处理器 next.ServeHTTP(w, r) }) } ``` 3. 在路由处理器中使用中间件。通过将中间件函数应用到路由处理器上,可以实现对该路由及其子路由的请求进行拦截和处理。 ```go func main() { // 创建一个路由器 router := mux.NewRouter() // 应用中间件到路由器 router.Use(LoggerMiddleware) // 添加路由处理器 router.HandleFunc("/", handler) // 启动服务器 http.ListenAndServe(":8080", router) } ``` 在上述例子中,`LoggerMiddleware` 是一个简单的日志记录中间件,它会在处理每个请求之前输出请求的路径信息。通过调用 `router.Use(LoggerMiddleware)`,该中间件会应用到所有的路由上。 这是一种常见的中间件实现原理,你可以根据自己的需求编写更复杂的中间件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值