深入浅出Go语言Map

简介: 深入浅出Go语言Map

Map在Go语言中一般被称为“字典”,他跟我们传统的哈希表差别并不是很大,但是也有些地方的设计和使用值得我们注意下,下面我们开始讲解~

1 使用方式

func NewMap() {
   //初始化方式1
   map1 := map[string]int{"A": 1, "B": 2}
   //初始化方式2
   map2 := make(map[string]int, 10)
   //初始化方式3
   map3 := new(map[string]int)
   map1["C"] = 3
   map2["A"] = 11
   map3 = &map[string]int{"A": 111}
   printMap(map1)
   printMap(map2)
   printMap(*map3)
}
func printMap(m map[string]int) {
  i := len(m)
  fmt.Println("len = ",i)
  for s := range m {
    fmt.Print(m[s], " ")
  }
  fmt.Println()
}
复制代码

运行结果:

网络异常,图片无法展示
|


遍历Map的K,V

func TestMap(t *testing.T) {
   m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 4, "E": 5}
   fmt.Println("First range ...")
   for s := range m {
      fmt.Println("key = ",s," ","val = ",m[s])
   }
   fmt.Println("Second range ...")
   for s := range m {
      fmt.Println("key = ",s," ","val = ",m[s])
   }
}
复制代码

运行结果:

网络异常,图片无法展示
|


2 原理探究

Go官网对Map的简单介绍:

  • Map是一种无序元素组,它由另一种类型的一组唯一键建立索引。未初始化映射的值为nil。
  • 比较操作符==和!=必须为键类型的操作数完全定义;因此键类型不能是函数、映射或片。
  • 如果键类型是接口类型,则必须为动态键值定义这些比较操作符;失败将导致panics。
  • map元素的数量称为它的长度。可以使用内置函数 len 发现它,并且在执行过程中可能会改变。
  • 元素可以在执行过程中使用assignments添加,使用index expressions检索;它们可以通过delete 内置函数删除。
  • 使用内置函数make 生成一个新的空map值,它接受map类型和一个可选的容量提示作为参数:
  • 初始容量不限制其大小:地图会增长以适应存储在其中的项目的数量,nil地图除外。一个nil映射相当于一个空映射,除了不能添加任何元素。

回到我们这里:

根据上述的运行结果,我们很容易发现:Map每次遍历后结果的顺序是不同的,为什么会这样?

这就要从Go语言中对Map的设计开始分析,首先看下源码${GO_PATH}/src/runtime/map.go

map数据结构:

// A header for a Go map.
type hmap struct {
   count     int // 元素个数
   flags     uint8
   B         uint8  //  buckets(桶)的数量的对数,也就是说该哈希表中桶的数量为 2^B 个
   noverflow uint16 // 溢出桶数
   hash0     uint32 // 哈希种子
   buckets    unsafe.Pointer // 指向 buckets 数组的指针,数组大小为 2^B,如果元素个数为 0,它为 nil。
   oldbuckets unsafe.Pointer // 指向老的buckets数组的指针,老的大小是新的1/2。非扩容状态为 nil。
   nevacuate  uintptr        // 表示扩容进度,小于此地址的 buckets 代表已搬迁完成
   extra *mapextra // extra 是指向 mapextra 类型的指针。
}
复制代码

mapextra结构:

type mapextra struct {
   // 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
   // 就使用 hmap 的 extr a字段来存储 overflow buckets,这样可以避免 GC 扫描整个 map
   // 然而 bmap.overflow 也是个指针。随意其实 bmap.overflow 的指针也是指向了
   // hmap.extra.overflow 和 hmap.extra.oldoverflow 中
   // overflow 包含的是 hmap.buckets 的 overflow 的 buckets
   // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket
   overflow    *[]*bmap
   oldoverflow *[]*bmap
   // 指向空闲的 overflow bucket 的指针
   nextOverflow *bmap
}
复制代码

map结构中的bucket结构:

type bmap struct {
   // Tophash通常包含散列值的顶字节对于这个bucket中的每个键。
   // 如果tophash[0] < minTopHash,Tophash[0]是桶清空状态。
   tophash [bucketCnt]uint8
}
复制代码

遍历Key源码:

func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) {
   if h == nil || h.count == 0 {
      return nil, nil
   }
   hash := t.hasher(key, uintptr(h.hash0))
   m := bucketMask(h.B)
   b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
   if c := h.oldbuckets; c != nil {
      if !h.sameSizeGrow() {
         // There used to be half as many buckets; mask down one more power of two.
         m >>= 1
      }
      oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
      if !evacuated(oldb) {
         b = oldb
      }
   }
   top := tophash(hash)
bucketloop:
   for ; b != nil; b = b.overflow(t) {
      for i := uintptr(0); i < bucketCnt; i++ {
         if b.tophash[i] != top {
            if b.tophash[i] == emptyRest {
               break bucketloop
            }
            continue
         }
         k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
         if t.indirectkey() {
            k = *((*unsafe.Pointer)(k))
         }
         if t.key.equal(key, k) {
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            if t.indirectelem() {
               e = *((*unsafe.Pointer)(e))
            }
            return k, e
         }
      }
   }
   return nil, nil
}
复制代码

网络异常,图片无法展示
|


(图片链接:img-blog.csdnimg.cn/img_convert…

由此,我们知道了为什么Map的遍历是无序的:

map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 就要变更位置了。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,有些 key 变更了bucket ,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。

那么Go语言中的Map是线程安全的吗?

非线程安全。但是go语言在sync包中提供了一种线程安全的map,我们下次再进行探讨

参考:

blog.csdn.net/weixin_4276…

blog.csdn.net/shunuanwei/…


相关文章
|
17天前
|
Go
go语言中的数据类型
go语言中的数据类型
13 0
|
22天前
|
Go 开发者
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
|
22天前
|
安全 Go
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
|
22天前
|
存储 缓存 安全
掌握Go语言:Go语言中的字典魔法,高效数据检索与应用实例解析(18)
掌握Go语言:Go语言中的字典魔法,高效数据检索与应用实例解析(18)
|
4天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
22天前
|
存储 安全 Go
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
|
1天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
1天前
|
SQL 关系型数据库 MySQL
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
|
1天前
|
Go 开发者
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
【4月更文挑战第21天】本文介绍了Go语言中的流程控制语句,包括`if`、`switch`和`for`循环。`if`语句支持简洁的语法和初始化语句,但需注意比较运算符的使用。`switch`语句提供多分支匹配,可省略`break`,同时支持不带表达式的形式。`for`循环有多种形式,如基本循环和`for-range`遍历,遍历时修改原集合可能导致未定义行为。理解并避免易错点能提高代码质量和稳定性。通过实践代码示例,可以更好地掌握Go语言的流程控制。
11 3
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
|
1天前
|
Go
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值
【4月更文挑战第21天】Go语言函数是代码组织的基本单元,用于封装可重用逻辑。本文介绍了函数定义(包括基本形式、命名、参数列表和多返回值)、调用以及匿名函数与闭包。在函数定义时,注意参数命名和注释,避免参数顺序混淆。在调用时,要检查并处理多返回值中的错误。理解闭包原理,小心处理外部变量引用,以提升代码质量和可维护性。通过实践和示例,能更好地掌握Go语言函数。
15 1
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值