golang map

这篇具有很好参考价值的文章主要介绍了golang map。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

golang 的 map 使用的是 hash map

基本结构

下面截取自源码,已翻译

// runtime/map.go:117

// go map 定义,hashmap 缩写
type hmap struct {
	count     int // map 里文件数
	flags     uint8 // map 当前是否在写入,一般为 hashWriting  = 4 (写入中)或 0 (空闲)
	B         uint8  // 桶的数量,2^B 个
	noverflow uint16 // 溢出桶的数量
	hash0     uint32 // hash 随机数,从 hmap 创建开始就不变

	buckets    unsafe.Pointer // 存储桶的指针,存放 2^B 个桶的地址
	oldbuckets unsafe.Pointer // 旧存储桶的地址,在扩容时候用
	nevacuate  uintptr        // 记录疏散桶进度 TODO

	extra *mapextra // 当 bucket 不含指针时,记录所有溢出桶的地址,加快 gc
}

golang map

// runtime/map.go:151

// 一个桶的设计
type bmap struct {
	tophash [bucketCnt]uint8 // 高 64 位存储每个 key 的信息
	// 后面跟 8 个 key
	// 然后跟 8 个 value
	// 下一个溢出桶地址
}

golang map

初始化

m  := make(map[int]string) // 初始化一个empty的 map,所有参数都是 0,用的方法是h := new(hmap) 参考runtime/map.go:294
m2 := make(map[int]string,100) // 初始化一个大小为 128 的 map
m3 := map[int]string{1:"a"} // 初始化 1 个桶的 map

我的golang是 1.21 版本(go version go1.21.1 darwin/arm64)
先分析使用 make 的情况,当 make 的第二个参数不填或者<=8的时候,调用的是 makemap_small(runtime/map.go:294) 函数,但我本地看汇编代码并没有发现 makemap_small 函数,应该是编译器有别的优化,具体可以深入研究一下,其他版本的 golang 还没测。
这里其实进行了很简单的内存分配和随机种子,所以找不到也可以理解

// runtime/map.go:294

func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand() // 随机 hash 种子
	return h
}

当第二个参数 >8的时候,会调用 makemap(runtime/map.go:305) 函数,这个从汇编代码可以看到,会进行 bucket 的内存分配

// runtime/map.go:305

func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// 这块跟 makemap_small是一样的
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()

	// 这块主要是计算桶的数量
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// 这块进行 bucket 的初始化
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

同时在 key 是 32 位,64 位,string 时,都用的同一样的创建 map,但是插入、读取、删除函数有各自的优化,否则就用通用的插入读取删除

插入

先对 key 进行 hash(hash 函数运行时得到,根据处理器有关,一般是 aes),得到 64 位的哈希值
用哈希值的低 B (hmap.B) 位来确定该 key 落入哪个桶内
再用哈希值的高 8 位寻找在 bucket 里的位置
然后依次找到空的位置,将 key,value 写入 bucekt 里的对应位置
如果当前 bucket 满了,会触发溢出桶,新建一个 bucket 的操作

golang map

读取

读取和插入类似,先对 key 进行 hash 运算得到 64 位 hash 值
然后依次计算低 B 位,高 8 位
然后找到对应的桶,再依次找高八位相同的值,再比较 key,所以能作为 map 的 key 的一定是可比较的类型,也就是支持==操作
如果找不到返回默认值

golang map

扩容

map 在元素增加过多或过于稀疏都会发生扩容

  1. 当装载因子大于 6.5,会发生扩容
  2. 当溢出桶的数量过多,但是装载因子却 < 6.5,说明map 比较稀疏,就需要sameSizeGrow,称作等量扩容

扩容

扩容就是增加一倍的 bucket 数量,把原来的某个 bucket 的元素重新取低B位,然后放到新的桶里

golang map

等量扩容

等量扩容也是走的扩容流程,只不过B 不+1,只是新建一个 bucket,将原来 bucket 里的数据搬迁到新的 bucket 里,当有多个溢出桶时候可能会压缩成一个,就没有溢出桶了

golang map

遍历

首先明确,map 在 range 遍历的时候返回的是值的拷贝,而不是原值,所以对遍历的值的修改对原值不会影响,如果遍历的 value 是指针的话,就相当于拿指针修改,就会有影响,但是 map 里存的值不会变

map 在遍历的时候先随机一个种子,然后从一个随机的 bucket 和随机的位置开始遍历
如果在扩容中,会查看 bucket 是否正在扩容,如果是正在扩容,回去老的 bucket 就是 oldbuckets 那遍历

golang map

难点

扩容

map 的主要难点就是扩容相关

并发

map 在写入时会将 flag 置为hashWriting,其他协程有写操作时候,会 panic

参考

深入解析Golang的map设计
逐行拆解 Go map 源码
Go语言基础结构 —— Map(哈希表)
golang map 从源码分析实现原理(go 1.14)
我可能并不会使用golang map
GO语言设计与实现-3.3哈希表
Go 程序员面试笔试宝典-哈希表文章来源地址https://www.toymoban.com/news/detail-746243.html

到了这里,关于golang map的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Golang三个编译基本命令

    在运行Go语言程序之前,先要将其编译成二进制的可执行文件 我们通常在编辑器编写完源码后使用go build或go run命令对GO语言程序进行编译 作用:将Go语言程序和相关依赖编译成可执行文件 语法格式:go build 【参数】 fileName 参数: fileName为所要编译的文件名,可以是一个或多

    2024年02月02日
    浏览(40)
  • Golang bitset 基本使用

    安装: 下面代码把fmtx换成fmt就行 布隆过滤器-假阳性率计算公式 可知在哈希函数的个数k一定的情况下: 位数组长度m越大,假阳性率越低; 已插入元素的个数n越大,假阳性率越高。 把fmtx删掉就行 目录格式 使用

    2024年02月13日
    浏览(45)
  • 二、GoLang输出HelloWorld、基本数据类型、变量常量定义、基本类型转换

    go语言中,想要输出内容到控制台,package必须是main,包括方法名也必须是main, go语言输出的语法是 fmt 库。 Go语言的基本类型有: boolean:布尔类型 true / false string :字符串类型 数值型: int8:有符号8位整型(-128到127)长度 int16:有符号16位整型(-32768到32767)长度 int32:有

    2024年02月09日
    浏览(59)
  • Golang 基本常量声明及 iota 使用

    普通声明时,与局部变量声明的方式一致: 多行常量声明时,如果常量值表达式为空,则会和前一个常量的值表达式相同。 iota 是常量声明时的一个自增的特殊变量; iota 在const 内部的第一行出现时,值为0,后续每新增一行,iota都会自增1。(可以理解为行索引或者行号)

    2024年02月13日
    浏览(45)
  • 【Golang】认识Go语言中基本的数据类型

    目录 整形 基本整型  特殊整型 浮点型 布尔型 字符型 字符串转义符  多行字符串 字符串的常用操作 复数 我们不论在学习什么语言中,我们都要去认识一下这个语言中的数据类型,当然学习Go也不例外,我们也要去认识一下其相关的数据类型,当然这些数据类型基本上是大

    2023年04月08日
    浏览(46)
  • golang学习-golang结构体和Json相互转换

    1、结构体转为json对象     v, _ := json.Marshal(student)     jsonStr := string(v) // 结构体转为json对象 2、json字符串转为结构体     var s1 Student     err := json.Unmarshal([]byte(str), s1) //json 字符串转为结构体    3、结构体标签 表示的是转换为json对象时,ID字段变为id,Name字段变为name. type

    2024年01月23日
    浏览(55)
  • Golang单元测试详解(一):单元测试的基本使用方法

    Golang 中的单元测试是使用标准库 testing 来实现的,编写一个单元测试是很容易的: 创建测试文件:在 Go 项目的源代码目录下创建一个新的文件(和被测代码文件在同一个包),以 _test.go 为后缀名。例如,要测试net包中 dial.go 中的方法,在 net 包中创建一个名字为 dial_test.g

    2024年02月06日
    浏览(49)
  • golang返回多层结构数据

    正常情况下,查询出的结果,基本上都是结构体所得 如图所表示,基本上查询出的两个结果 返回结果 比较好的办法是,data1和data2和data3等多个都包含在一个data里面,然后这个data和msg,以及code同一层级,而不是data1和data2和data3这些和msg以及code同一层级 解决办法: 可以事先

    2024年01月22日
    浏览(40)
  • Golang:Go语言结构

    在我们开始学习 Go 编程语言的基础构建模块前,让我们先来了解 Go 语言最简单程序的结构。 Go 语言的基础组成有以下几个部分: 包声明 引入包 函数 变量 语句 表达式 注释 接下来让我们来看下简单的代码,该代码输出了\\\"Hello World!\\\": 让我们来看下以上程序的各个部分: 第一

    2024年02月10日
    浏览(59)
  • golang学习-结构体

    1、定义 使用type 和struct 来定义结构体,是值类型 格式如下: type 类型名 struct {         字段名 类型         字段名 类型         ... } 2、实例化 1、var 结构体实例 结构体类型    var p1 Person   2、使用new   var p2 = new(Person) 3、使用对结构体进行取地址操作

    2024年01月22日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包