教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 万字长文全面解析Go是如何设计Map的

万字长文全面解析Go是如何设计Map的

发布时间:2023-03-07   编辑:jiaochengji.com
教程集为您提供万字长文全面解析Go是如何设计Map的等资源,欢迎您收藏本站,我们将为您提供最新的万字长文全面解析Go是如何设计Map的资源

由于本文篇幅较长,故将目录整理如下

什么是Map

维基百科的定义

In computer science, an associative array, map, symbol table, or dictionary is an abstract data type composed of a collection of (key, value) pairs, such that each possible key appears at most once in the collection.

说明:在计算机科学中,包含键值对(key-value)集合的抽象数据结构(关联数组、符号表或字典),其每个可能的键在该集合中最多出现一次,这样的数据结构就是一种Map。

操作

对Map的操作主要是增删改查:

  • 在集合中增加键值对
  • 在集合中移除键值对
  • 修改某个存在的键值对
  • 根据特定的键寻找对应的值

实现

Map的实现主要有两种方式:哈希表(hash table)和搜索树(search tree)。例如Java中的hashMap是基于哈希表实现,而C 中的Map是基于一种平衡搜索二叉树——红黑树而实现的。以下是不同实现方式的时间复杂度对比。

可以看到,对于元素查找而言,二叉搜索树的平均和最坏效率都是O(log n),哈希表实现的平均效率是O(1),但最坏情况下能达到O(n),不过如果哈希表设计优秀,最坏情况基本不会出现(所以,读者不想知道Go是如何设计的Map吗)。另外二叉搜索树返回的key是有序的,而哈希表则是乱序。

哈希表

由于Go中map的基于哈希表(也被称为散列表)实现,本文不探讨搜索树的map实现。以下是Go官方博客对map的说明。

One of the most useful data structures in computer science is the hash table. Many hash table implementations exist with varying properties, but in general they offer fast lookups, adds, and deletes. Go provides a built-in map type that implements a hash table.

学习哈希表首先要理解两个概念:哈希函数和哈希冲突。

哈希函数

哈希函数(常被称为散列函数)是可以用于将任意大小的数据映射到固定大小值的函数,常见的包括MD5、SHA系列等。

一个设计优秀的哈希函数应该包含以下特性:

  • 均匀性:一个好的哈希函数应该在其输出范围内尽可能均匀地映射,也就是说,应以大致相同的概率生成输出范围内的每个哈希值。
  • 效率高:哈希效率要高,即使很长的输入参数也能快速计算出哈希值。
  • 可确定性:哈希过程必须是确定性的,这意味着对于给定的输入值,它必须始终生成相同的哈希值。
  • 雪崩效应:微小的输入值变化也会让输出值发生巨大的变化。
  • 不可逆:从哈希函数的输出值不可反向推导出原始的数据。

哈希冲突

重复一遍,哈希函数是将任意大小的数据映射到固定大小值的函数。那么,我们可以预见到,即使哈希函数设计得足够优秀,几乎每个输入值都能映射为不同的哈希值。但是,当输入数据足够大,大到能超过固定大小值的组合能表达的最大数量数,冲突将不可避免!(参见抽屉原理)

抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。抽屉原理有时也被称为鸽巢原理。

如何解决哈希冲突

比较常用的Has冲突解决方案有链地址法和开放寻址法。

在讲链地址法之前,先说明两个概念。

  1. 哈希桶。哈希桶(也称为槽,类似于抽屉原理中的一个抽屉)可以先简单理解为一个哈希值,所有的哈希值组成了哈希空间。
  2. 装载因子。装载因子是表示哈希表中元素的填满程度。它的计算公式:装载因子=填入哈希表中的元素个数/哈希表的长度。装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。反之,装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数。装载因子也是决定哈希表是否进行扩容的关键指标,在java的HashMap的中,其默认装载因子为0.75;Python的dict默认装载因子为2/3。
链地址法

链地址法的思想就是将映射在一个桶里的所有元素用链表串起来。

下面以一个简单的哈希函数 H(key) = key MOD 7(除数取余法)对一组元素 [50, 700, 76, 85, 92, 73, 101] 进行映射,通过图示来理解链地址法处理Hash冲突的处理逻辑。

链地址法解决冲突的方式与图的邻接表存储方式在样式上很相似,发生冲突,就用单链表组织起来。

开放寻址法

对于链地址法而言,槽位数m与键的数目n是没有直接关系的。但是对于开放寻址法而言,所有的元素都是存储在Hash表当中的,所以无论任何时候都要保证哈希表的槽位数m大于或等于键的数据n(必要时,需要对哈希表进行动态扩容)。

开放寻址法有多种方式:线性探测法、平方探测法、随机探测法和双重哈希法。这里以线性探测法来帮助读者理解开放寻址法思想。

  • 线性探测法

Hash(key) 表示关键字 key 的哈希值, 表示哈希表的槽位数(哈希表的大小)。

线性探测法则可以表示为:

如果 Hash(x) % M 已经有数据,则尝试 (Hash(x) 1) % M ;

如果 (Hash(x) 1) % M 也有数据了,则尝试 (Hash(x) 2) % M ;

如果 (Hash(x) 2) % M 也有数据了,则尝试 (Hash(x) 3) % M ;

我们同样以哈希函数 H(key) = key MOD 7 (除数取余法)对 [50, 700, 76, 85, 92, 73, 101] 进行映射,通过图示来理解线性探测法处理 Hash 碰撞。

其中,empty代表槽位为空,occupied代表槽位已被占(后续映射到该槽,则需要线性向下继续探测),而lazy delete则代表将槽位里面的数据清除,并不释放存储空间。

两种解决方案比较

对于开放寻址法而言,它只有数组一种数据结构就可完成存储,继承了数组的优点,对CPU缓存友好,易于序列化操作。但是它对内存的利用率不如链地址法,且发生冲突时代价更高。当数据量明确、装载因子小,适合采用开放寻址法。

链表节点可以在需要时再创建,不必像开放寻址法那样事先申请好足够内存,因此链地址法对于内存的利用率会比开方寻址法高。链地址法对装载因子的容忍度会更高,并且适合存储大对象、大数据量的哈希表。而且相较于开放寻址法,它更加灵活,支持更多的优化策略,比如可采用红黑树代替链表。但是链地址法需要额外的空间来存储指针。

值得一提的是,在Python中dict在发生哈希冲突时采用的开放寻址法,而java的HashMap采用的是链地址法。

Go Map实现

同python与java一样,Go语言中的map是也基于哈希表实现的,它解决哈希冲突的方式是链地址法,即通过使用数组 链表的数据结构来表达map。

注意:本文后续出现的map统一代指Go中实现的map类型。

map数据结构

map中的数据被存放于一个数组中的,数组的元素是桶(bucket),每个桶至多包含8个键值对数据。**哈希值低位(low-order bits)用于选择桶,哈希值高位(high-order bits)用于在一个独立的桶中区别出键。**哈希值高低位示意图如下

本文基于go 1.15.2 darwin/amd64分析,源码位于src/runtime/map.go.

  • map的结构体为hmap
// A header for a Go map.
type hmap struct {
	count     int // 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
	flags     uint8 // 状态标志,下文常量中会解释四种状态位含义。
	B         uint8  // buckets(桶)的对数log_2(哈希表元素数量最大可达到装载因子*2^B)
	noverflow uint16 // 溢出桶的大概数量。
	hash0     uint32 // 哈希种子。

	buckets    unsafe.Pointer // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
	oldbuckets unsafe.Pointer // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2。非扩容状态下,它为nil。
	nevacuate  uintptr        // 表示扩容进度,小于此地址的buckets代表已搬迁完成。

	extra *mapextra // 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针。
  • mapextra的结构体
// mapextra holds fields that are not present on all maps.
type mapextra struct {
    // 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
    // 就使用 hmap的extra字段 来存储 overflow buckets,这样可以避免 GC 扫描整个 map
    // 然而 bmap.overflow 也是个指针。这时候我们只能把这些 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
}
  • bmap结构体
// A bucket for a Go map.
type bmap struct {
    // tophash包含此桶中每个键的哈希值最高字节(高8位)信息(也就是前面所述的high-order bits)。
    // 如果tophash[0] < minTopHash,tophash[0]则代表桶的搬迁(evacuation)状态。
    tophash [bucketCnt]uint8
}

bmap也就是bucket(桶)的内存模型图解如下(相关代码逻辑可查看src/cmd/compile/internal/gc/reflect.go中的bmap()函数)。

在以上图解示例中,该桶的第7位cell和第8位cell还未有对应键值对。需要注意的是,key和value是各自存储起来的,并非想象中的key/value/key/value…的形式。这样做虽然会让代码组织稍显复杂,但是它的好处是能让我们消除例如map[int64]int所需要的填充(padding)。此外,在8个键值对数据后面有一个overflow指针,因为桶中最多只能装8个键值对,如果有多余的键值对落到了当前桶,那么就需要再构建一个桶(称为溢出桶),通过overflow指针链接起来。

  • 重要常量标志
const (
	// 一个桶中最多能装载的键值对(key-value)的个数为8
	bucketCntBits = 3
	bucketCnt     = 1 << bucketCntBits

  // 触发扩容的装载因子为13/2=6.5
	loadFactorNum = 13
	loadFactorDen = 2

	// 键和值超过128个字节,就会被转换为指针
	maxKeySize  = 128
	maxElemSize = 128

	// 数据偏移量应该是bmap结构体的大小,它需要正确地对齐。
  // 对于amd64p32而言,这意味着:即使指针是32位的,也是64位对齐。
	dataOffset = unsafe.Offsetof(struct {
		b bmap
		v int64
	}{}.v)


  // 每个桶(如果有溢出,则包含它的overflow的链接桶)在搬迁完成状态(evacuated* states)下,要么会包含它所有的键值对,要么一个都不包含(但不包括调用evacuate()方法阶段,该方法调用只会在对map发起write时发生,在该阶段其他goroutine是无法查看该map的)。简单的说,桶里的数据要么一起搬走,要么一个都还未搬。
  // tophash除了放置正常的高8位hash值,还会存储一些特殊状态值(标志该cell的搬迁状态)。正常的tophash值,最小应该是5,以下列出的就是一些特殊状态值。
	emptyRest      = 0 // 表示cell为空,并且比它高索引位的cell或者overflows中的cell都是空的。(初始化bucket时,就是该状态)
	emptyOne       = 1 // 空的cell,cell已经被搬迁到新的bucket
	evacuatedX     = 2 // 键值对已经搬迁完毕,key在新buckets数组的前半部分
	evacuatedY     = 3 // 键值对已经搬迁完毕,key在新buckets数组的后半部分
	evacuatedEmpty = 4 // cell为空,整个bucket已经搬迁完毕
	minTopHash     = 5 // tophash的最小正常值

	// flags
	iterator     = 1 // 可能有迭代器在使用buckets
	oldIterator  = 2 // 可能有迭代器在使用oldbuckets
	hashWriting  = 4 // 有协程正在向map写人key
	sameSizeGrow = 8 // 等量扩容

	// 用于迭代器检查的bucket ID
	noCheck = 1<<(8*sys.PtrSize) - 1
)

综上,我们以B等于4为例,展示一个完整的map结构图。

创建map

map初始化有以下两种方式

make(map[k]v)
// 指定初始化map大小为hint
make(map[k]v, hint)

对于不指定初始化大小,和初始化值hint<=8(bucketCnt)时,go会调用makemap_small函数(源码位置src/runtime/map.go),并直接从堆上进行分配。

func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

当hint>8时,则调用makemap函数

// 如果编译器认为map和第一个bucket可以直接创建在栈上,h和bucket可能都是非空
// 如果h != nil,那么map可以直接在h中创建
// 如果h.buckets != nil,那么h指向的bucket可以作为map的第一个bucket使用
func makemap(t *maptype, hint int, h *hmap) *hmap {
  // math.MulUintptr返回hint与t.bucket.size的乘积,并判断该乘积是否溢出。
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
// maxAlloc的值,根据平台系统的差异而不同,具体计算方式参照src/runtime/malloc.go
	if overflow || mem > maxAlloc {
		hint = 0
	}

// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
  // 通过fastrand得到哈希种子
	h.hash0 = fastrand()

	// 根据输入的元素个数hint,找到能装下这些元素的B值
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B  
	}
	h.B = B

	// 分配初始哈希表
  // 如果B为0,那么buckets字段后续会在mapassign方法中lazily分配
	if h.B != 0 {
		var nextOverflow *bmap
    // makeBucketArray创建一个map的底层保存buckets的数组,它最少会分配h.B^2的大小。
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
    h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

分配buckets数组的makeBucketArray函数

// makeBucket为map创建用于保存buckets的数组。
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
	base := bucketShift(b)
	nbuckets := base
  // 对于小的b值(小于4),即桶的数量小于16时,使用溢出桶的可能性很小。对于此情况,就避免计算开销。
	if b >= 4 {
    // 当桶的数量大于等于16个时,正常情况下就会额外创建2^(b-4)个溢出桶
		nbuckets  = bucketShift(b - 4)
		sz := t.bucket.size * nbuckets
		up := roundupsize(sz)
		if up != sz {
			nbuckets = up / t.bucket.size
		}
	}

 // 这里,dirtyalloc分两种情况。如果它为nil,则会分配一个新的底层数组。如果它不为nil,则它指向的是曾经分配过的底层数组,该底层数组是由之前同样的t和b参数通过makeBucketArray分配的,如果数组不为空,需要把该数组之前的数据清空并复用。
	if dirtyalloc == nil {
		buckets = newarray(t.bucket, int(nbuckets))
	} else {
		buckets = dirtyalloc
		size := t.bucket.size * nbuckets
		if t.bucket.ptrdata != 0 {
			memclrHasPointers(buckets, size)
		} else {
			memclrNoHeapPointers(buckets, size)
		}
}

  // 即b大于等于4的情况下,会预分配一些溢出桶。
  // 为了把跟踪这些溢出桶的开销降至最低,使用了以下约定:
  // 如果预分配的溢出桶的overflow指针为nil,那么可以通过指针碰撞(bumping the pointer)获得更多可用桶。
  // (关于指针碰撞:假设内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”)
  // 对于最后一个溢出桶,需要一个安全的非nil指针指向它。
	if base != nbuckets {
		nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
		last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
		last.setoverflow(t, (*bmap)(buckets))
	}
	return buckets, nextOverflow
}

根据上述代码,我们能确定在正常情况下,正常桶和溢出桶在内存中的存储空间是连续的,只是被 hmap 中的不同字段引用而已。

哈希函数

在初始化go程序运行环境时(src/runtime/proc.go中的schedinit),就需要通过alginit方法完成对哈希的初始化。

func schedinit() {
	lockInit(&sched.lock, lockRankSched)
	
	...
	
	tracebackinit()
	moduledataverify()
	stackinit()
	mallocinit()
	fastrandinit() // must run before mcommoninit
	mcommoninit(_g_.m, -1)
	cpuinit()       // must run before alginit
	// 这里调用alginit()
	alginit()       // maps must not be used before this call
	modulesinit()   // provides activeModules
	typelinksinit() // uses maps, activeModules
	itabsinit()     // uses activeModules
	
	...

	goargs()
	goenvs()
	parsedebugvars()
	gcinit()
  
  ...
}

对于哈希算法的选择,程序会根据当前架构判断是否支持AES,如果支持就使用AES hash,其实现代码位于src/runtime/asm_{386,amd64,arm64}.s中;若不支持,其hash算法则根据xxhash算法(https://code.google.com/p/xxhash/)和cityhash算法(https://code.google.com/p/cityhash/)启发而来,代码分别对应于32位(src/runtime/hash32.go)和64位机器(src/runtime/hash32.go)中,对这部分内容感兴趣的读者可以深入研究。

func alginit() {
	// Install AES hash algorithms if the instructions needed are present.
	if (GOARCH == "386" || GOARCH == "amd64") &&
		cpu.X86.HasAES && // AESENC
		cpu.X86.HasSSSE3 && // PSHUFB
		cpu.X86.HasSSE41 { // PINSR{D,Q}
		initAlgAES()
		return
	}
	if GOARCH == "arm64" && cpu.ARM64.HasAES {
		initAlgAES()
		return
	}
	getRandomData((*[len(hashkey) * sys.PtrSize]byte)(unsafe.Pointer(&hashkey))[:])
	hashkey[0] |= 1 // make sure these numbers are odd
	hashkey[1] |= 1
	hashkey[2] |= 1
	hashkey[3] |= 1
}

上文在创建map的时候,我们可以知道map的哈希种子是通过h.hash0 = fastrand()得到的。它是在以下maptype中的hasher中被使用到,在下文内容中会看到hash值的生成。

type maptype struct {
	typ    _type
	key    *_type
	elem   *_type
	bucket *_type
  // hasher的第一个参数就是指向key的指针,h.hash0 = fastrand()得到的hash0,就是hasher方法的第二个参数。
  // hasher方法返回的就是hash值。
	hasher     func(unsafe.Pointer, uintptr) uintptr
	keysize    uint8  // size of key slot
	elemsize   uint8  // size of elem slot
	bucketsize uint16 // size of bucket
	flags      uint32
}

map操作

假定key经过哈希计算后得到64bit位的哈希值。如果B=5,buckets数组的长度,即桶的数量是32(2的5次方)。

例如,现要置一key于map中,该key经过哈希后,得到的哈希值如下:

前面我们知道,哈希值低位(low-order bits)用于选择桶,哈希值高位(high-order bits)用于在一个独立的桶中区别出键。当B等于5时,那么我们选择的哈希值低位也是5位,即01010,它的十进制值为10,代表10号桶。再用哈希值的高8位,找到此key在桶中的位置。最开始桶中还没有key,那么新加入的key和value就会被放入第一个key空位和value空位。

注意:对于高低位的选择,该操作的实质是取余,但是取余开销很大,在实际代码实现中采用的是位操作。以下是tophash的实现代码。

func tophash(hash uintptr) uint8 {
	top := uint8(hash >> (sys.PtrSize*8 - 8))
	if top < minTopHash {
		top  = minTopHash
	}
	return top
}

当两个不同的key落在了同一个桶中,这时就发生了哈希冲突。go的解决方式是链地址法(这里为了让读者更好理解,只描述非扩容且该key是第一次添加的情况):在桶中按照顺序寻到第一个空位并记录下来,后续在该桶和它的溢出桶中均未发现存在的该key,将key置于第一个空位;否则,去该桶的溢出桶中寻找空位,如果没有溢出桶,则添加溢出桶,并将其置溢出桶的第一个空位(因为是第一次添加,所以不描述已存在该key的情况)。

上图中的B值为5,所以桶的数量为32。通过哈希函数计算出待插入key的哈希值,低5位哈希00110,对应于6号桶;高8位10010111,十进制为151,由于桶中前6个cell已经有正常哈希值填充了(遍历),所以将151对应的高位哈希值放置于第7位cell,对应将key和value分别置于相应的第七个空位。

如果是查找key,那么我们会根据高位哈希值去桶中的每个cell中找,若在桶中没找到,并且overflow不为nil,那么继续去溢出桶中寻找,直至找到,如果所有的cell都找过了,还未找到,则返回key类型的默认值(例如key是int类型,则返回0)。

查找key

对于map的元素查找,其源码实现如下

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  // 如果开启了竞态检测 -race
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := funcPC(mapaccess1)
		racereadpc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
  // 如果开启了memory sanitizer -msan
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
  // 如果map为空或者元素个数为0,返回零值
	if h == nil || h.count == 0 {
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
  // 注意,这里是按位与操作
  // 当h.flags对应的值为hashWriting(代表有其他goroutine正在往map中写key)时,那么位计算的结果不为0,因此抛出以下错误。
  // 这也表明,go的map是非并发安全的
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
  // 不同类型的key,会使用不同的hash算法,可详见src/runtime/alg.go中typehash函数中的逻辑
	hash := t.hasher(key, uintptr(h.hash0))
	m := bucketMask(h.B)
  // 按位与操作,找到对应的bucket
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
  // 如果oldbuckets不为空,那么证明map发生了扩容
  // 如果有扩容发生,老的buckets中的数据可能还未搬迁至新的buckets里
  // 所以需要先在老的buckets中找
	if c := h.oldbuckets; c != nil {
		if !h.sameSizeGrow() {
			m >>= 1
		}
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
    // 如果在oldbuckets中tophash[0]的值,为evacuatedX、evacuatedY,evacuatedEmpty其中之一
    // 则evacuated()返回为true,代表搬迁完成。
    // 因此,只有当搬迁未完成时,才会从此oldbucket中遍历
		if !evacuated(oldb) {
			b = oldb
		}
	}
  // 取出当前key值的tophash值
	top := tophash(hash)
  // 以下是查找的核心逻辑
  // 双重循环遍历:外层循环是从桶到溢出桶遍历;内层是桶中的cell遍历
  // 跳出循环的条件有三种:第一种是已经找到key值;第二种是当前桶再无溢出桶;
  // 第三种是当前桶中有cell位的tophash值是emptyRest,这个值在前面解释过,它代表此时的桶后面的cell还未利用,所以无需再继续遍历。
bucketloop:
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i   {
      // 判断tophash值是否相等
			if b.tophash[i] != top {
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
      }
      // 因为在bucket中key是用连续的存储空间存储的,因此可以通过bucket地址 数据偏移量(bmap结构体的大小)  keysize的大小,得到k的地址
      // 同理,value的地址也是相似的计算方法,只是再要加上8个keysize的内存地址
			k := add(unsafe.Pointer(b), dataOffset i*uintptr(t.keysize))
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
      // 判断key是否相等
			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 e
			}
		}
	}
  // 所有的bucket都未找到,则返回零值
	return unsafe.Pointer(&zeroVal[0])
}

以下是mapaccess1的查找过程图解

map的元素查找,对应go代码有两种形式

	// 形式一
	v := m[k]
	// 形式二
	v, ok := m[k]

形式一的代码实现,就是上述的mapaccess1方法。此外,在源码中还有个mapaccess2方法,它的函数签名如下。

func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {}

与mapaccess1相比,mapaccess2多了一个bool类型的返回值,它代表的是是否在map中找到了对应的key。因为和mapaccess1基本一致,所以详细代码就不再贴出。

同时,源码中还有mapaccessK方法,它的函数签名如下。

func mapacces

您可能感兴趣的文章:
golang中map的一些注意事项
想系统学习GO语言(Golang
Go语言发展历史、核心、特性及学习路线
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)
golang 并发访问map遇到的问题
go 获取函数地址_Go语言基础--接口浅析
Go基础编程:Map
由浅入深聊聊Golang的map
「对比Python学习Go」- 高级数据结构下篇
Go编程基础-学习1

[关闭]
~ ~