标题
- 前言
- [1. map 结构](#1. map 结构)
- [2. Group 结构](#2. Group 结构)
-
- [2.1 字段定义](#2.1 字段定义)
- [2.2 相关方法](#2.2 相关方法)
-
- [2.2.1 bitset](#2.2.1 bitset)
- [2.2.2 ctrlGroup](#2.2.2 ctrlGroup)
- [2.2.3 groupReference](#2.2.3 groupReference)
- [2.2.4 groupsReference](#2.2.4 groupsReference)
- [3. 小结](#3. 小结)
前言
目前正在学习 Go 相关数据结构的源码。之前研究 Java 时,第一个深入分析的源码就是 HashMap,因此这次学习 Go 也以 map 为起点,下面是之前 Java 的 HashMap 介绍。- 第一篇文章:源码解析系列:HashMap(1)
- 第二篇文章:源码解析系列:HashMap(2)
- 第三篇文章:源码解析系列:HashMap(3)
- 第四篇文章:源码解析系列:HashMap(4)
- 第五篇文章:源码解析系列:HashMap(5)
下面是 Go 的 map 介绍。
- 第一篇文章:【Go】map 使用
1. map 结构

上面是 Go 的简单示例图,最顶层的就是 Map,Map 下面有两个分支:
- Small Map :如果用一个 group 就能装下所有 k-v,那么就不需要多个 group,此时 map 类型就是
small map,检索速度更快。 - Normal Map :如果一个 map 里的 k-v 数量超过 8 个,就需要多个 group,此时 map 结构为
Map -> Directory -> Group。可以把 Directory 理解为书页,查询时需要先计算出当前 key 属于哪一页(哪个 group),再进入该 group 内查询。
2. Group 结构
上面介绍了 Map 的整体结构,下面来详细说明 group 的结构,group 的源码路径在 internal\runtime\maps\group.go。
2.1 字段定义
group 的内存结构如下:[ ctrls 控制字 ][ slot0 ][ slot1 ][ slot2 ]...,其中 ctrls 控制字 是 int64 类型的,8 个字节,对应 8 个 slot,也就是说一个 group 里面有 8 个 slot。slot 就是一个 Elem,也就是存储 key-value,但是要注意 go 里面并没有明确声明 slot 这个结构体,而是通过地址访问。实际上 group 的整体结构可以看成下面的结构体。
go
// type group struct {
// ctrls ctrlGroup
// slots [abi.MapGroupSlots]slot
// }
//
// type slot struct {
// key typ.Key
// elem typ.Elem
// }
ctrls 里面每一个控制字节都可以用 ctrl 来表示,在源码中的定义如下。
go
// 哈希表中的每个 Slot 都有一个控制字节,用于表示 Slot 的状态。
// 控制字节有三种状态:空、已删除和已占用,编码如下:
//
// 空:1 0 0 0 0 0 0 0
// 已删除:1 1 1 1 1 1 1 0
// 已占用:0 h h h h h h h // h 表示哈希值的 H2 部分
//
// TODO(prattmic):考虑将最高位取反,使零值可以表示空状态。
type ctrl uint8
// ctrlGroup 表示一个 Group 中的固定数量控制字节,
// 并将这些控制字节整体存储在一个 uint64 中。
type ctrlGroup uint64
其中 ctrl 代表一个 slot 的状态,group 中每一个 slot 都有三种状态:
- ctrlEmpty:
0b10000000,代表这个 slot 是空的,可以往里面写入 key-value。 - ctrlDeleted:
0b11111110,代表这个 slot 原来里面有 key-value,但是被删除了,因此将这个 slot 标记为 已删除。 - ctrlFull: 源码里面没有定义这个状态,如果 slot 里面写入了 key-value,那么这个 slot 的状态会被标记成
0b 0hhhhhhh,其中后面的 7 个 h 就是 key 的 h2 部分。
根据上面三种状态就能简单判断出一个 slot 到底是 empty、deleted 还是 full,从而判断这个 slot 是否能继续写入,这个是后面的逻辑了。
当然,一个 ctrlGroup 是 64 位,在遍历一个 group 查询里面的 key-value,如何快速知道这个 group 里面哪些 slot 里面是满足条件的,源码中就提供了 type bitset uint64,当然这个字段不是用来存当前有哪些 slot 是空的,哪些 slot 是满的。当调用 matchEmpty、matchEmptyOrDeleted 的时候会先根据 group 里面的 slot 内容生成一个 bitset,然后遍历这个 bitset 来遍历 slot。
go
// bitset 表示 Group 中一组 Slot 的集合,用来简单记录下 slot 的状态,比如简单
// 记录下这个 slot 上面有没有数据,这样可以快速返回有哪些 slot 上面有 key-value
type bitset uint64
在不同架构下,这个 bitset 的逻辑不同:
- 在 AMD64 架构上,bitset 为每个 slot 只使用 1 位,也就是总共 8 bit,此时 bitset 只用最低 8 位记录,高 56 位没用,最终结果就是
0b00100101。 - 在其他通用结构中,每个 slot 用一个字节(8 bit),否则为
0x00。
所以为了兼容这两种情况,使用了 uint64 来兼容上面这两种情况,那 为什么 AMD64 只用 8 bit? AMD64 上有专门的 SIMD / 位操作指令,可以一次比较 8 个控制字节,并直接生成一个 匹配掩码 :
go
8 个控制字节: [C0] [C1] [C2] [C3] [C4] [C5] [C6] [C7]
比较后直接得到:[ 1 ] [ 0 ] [ 1 ] [ 0 ] [ 0 ] [ 1 ] [ 0 ] [ 0 ]
AMD64 这种计算方式也比较方便,压缩成 1 个字节,相当于直接操作二进制,
2.2 相关方法
下面是 group.go 里面针对上面几个状态获取的相关接口。
2.2.1 bitset
首先是 bitset 相关的方法,first 返回 bitset 中第一个控制字节在当前 Group 中的相对索引。
go
// first 返回集合中第一个控制字节在当前 Group 中的相对索引。
//
// 前置条件:bitset 不为空,也就是里面有数据
func (b bitset) first() uintptr {
return bitsetFirst(b)
}
// first 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,直接计算末尾连续 0 的数量。
// 由于 bitset 已经是打包表示,因此不需要再进行移位,而通用架构上由于每个 slot
// 都是一个字节,所以计算出来之后需要 >> 3
// - 10000000 00000000 10000000 00000000
// -> 15 >> 3 = 1
// 也就是下标 1,第二个
func bitsetFirst(b bitset) uintptr {
return uintptr(sys.TrailingZeros64(uint64(b))) >> 3
}
bitsetFirst 会计算出从右往左遇到第一个 1 之前,遇到了多少个 0,比如上面的例子算出来的就是 15,然后 15 >> 3 = 15 / 8 = 1,就是下标 1 的 slot。
go
func (b bitset) removeFirst() bitset {
return b & (b - 1)
}
上面的 removeFirst 就是清除掉第一个 1,也就是将最低位的 1 清除。
go
// removeBelow 清除 Slot i 之前的所有已设置位,但不清除 Slot i。
func (b bitset) removeBelow(i uintptr) bitset {
return bitsetRemoveBelow(b, i)
}
// removeBelow 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,用于清除低 i 位。
// Slot: S3 S2 S1 S0
// bitset: 10000000 00000000 10000000 00000000
// 比如 i = 2,那么这个方法会清掉 S0 和 S1,保留 S2 和 S3
func bitsetRemoveBelow(b bitset, i uintptr) bitset {
// 1. 首先计算出 i 的位移量: 8 * 2 = 16
// 2. 1 << 16 - 1 -> 00000000 00000001 00000000 00000000
// 3. mask = 00000000 00000000 11111111 11111111
// 4. &^ 是 go 的特殊写法,代表 b & (!mask),也就是 and not
mask := (uint64(1) << (8 * uint64(i))) - 1
return b &^ bitset(mask)
}
removeBelow 用于清除 bitset 中所有下标 < i 的位,只保留下标 ≥ i 的位。
go
// lowestSet 判断 bitset 中索引最低的 Slot 对应的位是否已设置,也就是 S0
//
// 该方法与 shiftOutLowest 配合使用,用于遍历所有 Slot,无论当前 Slot 是
// 否属于 bitset 表示的集合。
func (b bitset) lowestSet() bool {
return bitsetLowestSet(b)
}
// lowestSet 的通用实现。
//
// 在 AMD64 架构上,该函数会被替换为编译器内建实现,用于检查 S0 是否设置了值
func bitsetLowestSet(b bitset) bool {
return b&(1<<7) != 0
}
lowestSet 用于判断最低位是否已设置。
go
// count 返回 b 中已设置 Slot 的数量,也就是统计一个 64 位整数的二进制表示中,
// 有多少个 bit 是 1。
func (b bitset) count() int {
// AMD64 和通用 bitset 表示都适用。
return sys.OnesCount64(uint64(b))
}
最后 count 用于计算 bitset 里面有多少个 1。
2.2.2 ctrlGroup
go
// ctrlGroup 表示一个 Group 中的固定数量控制字节,
// 并将这些控制字节整体存储在一个 uint64 中。
type ctrlGroup uint64
// get 返回第 i 个控制字节。
func (g *ctrlGroup) get(i uintptr) ctrl {
if goarch.BigEndian {
return *(*ctrl)(unsafe.Add(unsafe.Pointer(g), 7-i))
}
return *(*ctrl)(unsafe.Add(unsafe.Pointer(g), i))
}
// set 设置第 i 个控制字节。
func (g *ctrlGroup) set(i uintptr, c ctrl) {
if goarch.BigEndian {
*(*ctrl)(unsafe.Add(unsafe.Pointer(g), 7-i)) = c
return
}
*(*ctrl)(unsafe.Add(unsafe.Pointer(g), i)) = c
}
上面是最基础的 get,set 方法,我们之前说过,group 的布局结构是 【ctrlGroup】【slot0】【slot1】...【slot n】,所以计算第 i 个 slot 的状态的方法就是计算出 group 的地址再加上第 i 个 bit,也就是 unsafe.Add(unsafe.Pointer(g), i)。
go
bitsetLSB = 0x0101010101010101
bitsetEmpty = bitsetLSB * uint64(ctrlEmpty)
// setEmpty 将所有控制字节设置为空状态。
func (g *ctrlGroup) setEmpty() {
*g = ctrlGroup(bitsetEmpty)
}
setEmpty 将所有控制字节设置为空状态,这里注意直接赋值给 *g,因为 group 的起始地址就是 ctrlsGroup 的地址。
go
// matchH2 返回当前 Group 中满足以下条件的 Slot 集合:
// 1. Slot 处于已占用状态;
// 2. Slot 控制字节中的 7 位 H2 与给定值匹配。
// 该方法可能返回误匹配结果,因此调用方仍然必须比较完整的 key。
func (g ctrlGroup) matchH2(h uintptr) bitset {
return ctrlGroupMatchH2(g, h)
}
// matchH2 的通用实现。参数 h 就是 h2,g 就是控制字段。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 返回值是 bitset 类型,其实就是 unit64
func ctrlGroupMatchH2(g ctrlGroup, h uintptr) bitset {
// bitsetLSB:0x0101010101010101
// -> 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001
// bitsetMSB: 0x8080808080808080
// -> 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000
//
// 1. bitsetLSB * uint64(h) 就是把 h 乘到 bitsetLSB 中,比如 h = 0x20,那么相乘之后结果就
// 是 0x0202020202020202
// - 这里解释下,因为 bitsetLSB 是 0x0101010101010101,其实就是 0x01 * (2^0 + 2^8 + 2^16 + 2^24 + 2^32 + 2^40 + 2^48 + 2^56),
// 所以 h = 0x20 相乘之后结果就是 0x0202020202020202,也就是 0x20 * (2^0 + 2^8 + 2^16 + 2^24 + 2^32 + 2^40 + 2^48 + 2^56),
// 因此相乘之后就相当于将 h 复制到 8 个字节中,为什么不会产生进位?h 最大不过是 0xFF,且最关键的 bitsetLSB 相当于 1 * h,因此乘完
// 之后最大就是 0xFFFFFFFF,不超过一个字节.
// 2. uint64(g) ^ (bitsetLSB * uint64(h)) 计算异或值,我们之前说过,控制字段的值大概就是 0bxxxxxxxx xxxxxxxx xxxxxxxx ... 这种格式,
// 每 8 位在 Slot 不为空的时候设置的就是 key 的 h2,进行 ^ 匹配,由于相同的会变成 0,因此如果能和这 8 个 Slot 其中某一个匹配上,那么
// 肯定会出现 0bxxxxxxxx 00000000 ........ 这种情况.
// 3. v - bitsetLSB 会把 00000000 变成 FFFFFFFF,也就是说符合条件的 slot 对应的 8 个 bit 就是 FFFFFFFF,比如 xxxxxxxx FFFFFFFF ........
// 4. (v - bitsetLSB) &^ v 就是 (v - bitsetLSB) & (~v),由于 v 里面符合条件的 slot 是 00000000,所以 ~v 就是 FFFFFFFF,跟第三步的 &,
// 结果就是让符合条件的保留 FFFFFFFF,其他的位不管是多少,- bitsetLSB 之后最高位肯定不是 1
// 5. bitset(((v - bitsetLSB) &^ v) & bitsetMSB) 就是提取最高位了,这种情况下只有符合条件的 slot 最高位才是 1
//
v := uint64(g) ^ (bitsetLSB * uint64(h))
return bitset(((v - bitsetLSB) &^ v) & bitsetMSB)
}
ctrlGroupMatchH2 用于返回一个 bitset,ctrlGroup 里面有 8 个 slot 的状态,这个方法返回的 bitset 就是标识 ctrlGroup 里面有哪些 slot 跟传进来的 h 匹配,这个方法用来匹配 h2 的,也就是根据 key 的 h2 快速判断这 8 个 slot 里面哪些 slot 是跟 h2 相等的,这些就有可能是我们要找的 key-value。
go
// matchEmpty 返回当前 Group 中处于空状态的 Slot 集合。
func (g ctrlGroup) matchEmpty() bitset {
return ctrlGroupMatchEmpty(g)
}
// matchEmpty 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchEmpty(g ctrlGroup) bitset {
// 空 Slot:1 0 0 0 0 0 0 0
// 已删除 Slot:1 1 1 1 1 1 1 0
// 已占用 Slot:0 ? ? ? ? ? ? ?
//
// 一个 Slot 是不是空的,主要看第 1 位是不是 0 和第 7 位是不是 1
// bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
// << 6 之后 bit1 就去到了 bit7 的位置
// &^ 就是 & (not (v << 6)),空 Slot 的 bit1 移动到 bit7 之后,经过 not 就是 1 了
// (v &^ (v << 6)),求出结果再和 bitsetMSB 相 &,最高位如果是 1 就是 Empty 了
v := uint64(g)
return bitset((v &^ (v << 6)) & bitsetMSB)
}
matchEmpty 用于返回当前 Group 中处于空状态的 Slot 集合,空状态是 0b10000000。
java
// matchEmptyOrDeleted 返回当前 Group 中处于空状态或已删除状态的 Slot 集合。
func (g ctrlGroup) matchEmptyOrDeleted() bitset {
return ctrlGroupMatchEmptyOrDeleted(g)
}
// matchEmptyOrDeleted 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchEmptyOrDeleted(g ctrlGroup) bitset {
// 空 Slot:1 0 0 0 0 0 0 0
// 已删除 Slot:1 1 1 1 1 1 1 0
// 已占用 Slot:0 ? ? ? ? ? ? ?
//
// 只要第 7 位为 1,Slot 就处于空状态或已删除状态。
v := uint64(g)
return bitset(v & bitsetMSB)
}
matchEmptyOrDeleted 用于返回当前 Group 中处于 Empty 或者 Deleted 状态的 Slot 集合,空状态是 0b10000000,deleted 状态是 0b11111110。
go
bitsetMSB = 0x8080808080808080
// anyFull 判断当前 Group 中是否至少有一个 Slot 处于已占用状态。
func (g ctrlGroup) anyFull() bool {
// Slot 处于已占用状态的条件是第 7 位为 0。
// 检查是否存在第 7 位为 0 的控制字节。
return (^g)&bitsetMSB != 0
}
anyFull 判断当前 Group 中是否至少有一个 Slot 处于已占用状态,已占用的意思就是 0b0hhhhhhh,可以看到最高位是 0,所以 ^g & 10000000 != 0。
go
// matchFull 返回当前 Group 中处于已占用状态的 Slot 集合。
func (g ctrlGroup) matchFull() bitset {
return ctrlGroupMatchFull(g)
}
// matchFull 的通用实现。
//
// 注意:在 AMD64 架构上,该方法会被替换为使用 SIMD 指令的编译器内建实现。
// 关于这种打包后的返回值,请参见 bitset 的说明。
func ctrlGroupMatchFull(g ctrlGroup) bitset {
// 空 Slot:1 0 0 0 0 0 0 0
// 已删除 Slot:1 1 1 1 1 1 1 0
// 已占用 Slot:0 ? ? ? ? ? ? ?
//
// 只要第 7 位为 0,Slot 就处于已占用状态。
v := uint64(g)
return bitset(^v & bitsetMSB)
}
上面 anyFull 是返回当前 groups 里面是否有已经占用的 slot,这里的 matchFull 就是返回当前 groups 里面有哪些已经占用的 slot。
2.2.3 groupReference
下面是 group 的结构体描写,可以看到里面并没有完整把 slot 结构描述出来,而是只提供了 group 的起始地址,这是因为 slot 的具体类型要等到编译期才能确定------map 的 key/value 类型本来就是不确定的,所以 Go 是通过编译器和运行时提供的类型信息,来动态描述实际的 group 布局的。
一个 group 里包含两部分内容:控制字节和 slots。
- 控制字节是一个 long 类型,由于一个 group 共有 8 个 slot,所以 long 里的每 8 bit 就代表一个 slot 的状态。控制字节并不存储完整的 key/value,它保存的是 slot 的摘要信息,主要用来解决两个问题:这个 slot 当前是什么状态,以及如果 slot 被占用,它对应的 key 有没有可能是我们要找的 key。
- slots 部分则真正存放具体的 key-value,一个 group 有 8 个 slot。
下面具体说下控制字节。一个 slot 有三种状态:
- 第一,这个 slot 是空的(Empty);
- 第二,这个 slot 以前有元素,但已经被删除了(Deleted);
- 第三,这个 slot 正在使用中(Full),此时会记录 key 哈希的相关信息。
控制字节和 slot 是一一对应的,C0 对应 S0,C1 对应 S1,依此类推。
当第一次往某个位置添加元素时,这个 slot 的状态就是 Full,此时它的控制字节形如 0xxxxxxx:最高位是 0,剩下的 7 bit 保存 key 哈希值的低 7 位,也就是 H2。比如某个 key 的 H2 是 0101010,那么这个 slot 的控制字节就是 00101010。
接下来如果删除元素,这个位置就会变成 0xFE,表示 Deleted 状态。为什么要这样处理呢?因为 Go 的 map 查询 key-value 用的是 线性探测法 :先根据 hash 找到一个 slot 下标,然后从这个位置开始线性往后探测,直到遇到空位才停止。如果删除元素时直接把这个位置改成 Empty,那么线性探测一旦走到这里就会停下,可实际上它后面可能还有数据,这样就会漏掉本该找到的元素。所以删除时用 0xFE 这个特殊的 Deleted 状态来占位,既标记了此处已删除,又不会让探测提前中断。
go
type groupReference struct {
// 下面就是指向分配出来的 group 内存,通过下面的 ctrls()/key()/elem() 访问地址对应的数据,
// group 的整体结构如下
//
// type group struct {
// ctrls ctrlGroup
// slots [abi.MapGroupSlots]slot
// }
//
// type slot struct {
// key typ.Key
// elem typ.Elem
// }
//
// Group 起始地址
// │
// ├── ctrlGroup
// │ ├── control byte 0
// │ ├── control byte 1
// │ ├── ...
// │ └── control byte 7
// │
// ├── Slot 0
// │ ├── key:string
// │ └── elem:int
// │
// ├── Slot 1
// │ ├── key:string
// │ └── elem:int
// │
// ├── ...
// │
// └── Slot 7
// ├── key:string
// └── elem:int
data unsafe.Pointer // data *typ.Group
}
我们接下来继续看下这个结构体对应的一些方法。
go
/**
* group 的内存布局如下:
* - [ ctrls 控制字 ][ slot0 ][ slot1 ][ slot2 ]...
*
* 例如,假设某个 group 的起始地址是 0x1000,
* 那么:
* - ctrls() 返回的就是 0x1000 这 8 个字节对应的控制字区域
* - key(typ, 0) 返回 slot0 中 key 的地址
* - elem(typ, 0) 返回 slot0 中 elem 的地址
* - key(typ, 1) 返回 slot1 中 key 的地址
*
* 通过下面的 ctrls / key / elem 方法,可以按 slot 下标访问这段连续内存。
*/
// ctrls 返回当前 group 的控制字节区域。
//
// 例子:
// - 如果 g.data 指向某个 group 的起始地址 0x1000,
// 那么 ctrls() 返回的就是位于 0x1000 开头的 ctrlGroup。
func (g *groupReference) ctrls() *ctrlGroup {
return (*ctrlGroup)(g.data)
}
首先就是 ctrls,这个方法用来返回 group 的控制字节,因为整个 group 的起始地址就是 ctrls 的起始地址,所以直接获取地址然后转成 *ctrlGroup 类型的就行了。
go
const (
ctrlGroupsSize = unsafe.Sizeof(ctrlGroup(0))
groupSlotsOffset = ctrlGroupsSize
)
// key 返回下标 i 的 slot 元素的地址,而由于 key 在前面,所以这里返回的也是 key 的地址
func (g *groupReference) key(typ *abi.MapType, i uintptr) unsafe.Pointer {
// [ctrls 控制字] + i * 每个 slot 占用的大小 就是第 i 个 slot 的地址偏移量
offset := groupSlotsOffset + i*typ.SlotSize
// 起始偏移量(groupReference 的地址) + 偏移量
return unsafe.Pointer(uintptr(g.data) + offset)
}
// elem 内存布局是 [ key ][ elem ],ElemOff 是 slot 里面起始位置到 slot 的大小,
// 实际上这个方法返回的就是第 i 个 slot 里面 elem 的地址
func (g *groupReference) elem(typ *abi.MapType, i uintptr) unsafe.Pointer {
offset := groupSlotsOffset + i*typ.SlotSize + typ.ElemOff
return unsafe.Pointer(uintptr(g.data) + offset)
}
接下来就是 group 最重要的两个方法,查找 key 的地址和 value 的地址,key 的地址很简单,就是 ctrlGroupsSize + i * slotSize,因为 slot 的起始位置就是 key 的起始位置。elem 的计算差不多,要注意一点就是 ElemOff 是 elem 在 slot 里面的偏移量,所以直接加上就行了。
2.2.4 groupsReference
groupReference 是 group 结构体,这里就是 groups 结构体,存储多个 group,Map 直接引用的就是这个结构体。
go
// groupsReference 是对一段连续 Group 数组的引用封装,map 里面引用 group 就是通过
// 下面的结构体,Group 数组的实际地址保存在 data 中
type groupsReference struct {
// data 指向 Group 数组的起始地址,这里的实际类型可以理解为 *[length]typ.Group,
// 其中每个 Group 的具体布局由 map 的 key/value 类型决定。
data unsafe.Pointer // data *[length]typ.Group
// lengthMask 等于 Group 数量减 1,因为 Group 数量必须是 2 的幂,因此可以使用 i
// & lengthMask 代替较慢的取模运算 i % length
lengthMask uint64
}
接下来就是初始化方法,也就是 new 方法。
go
// 新建一个 group 数组,数组长度是 length
//
// Length must be a power of two.
func newGroups(typ *abi.MapType, length uint64) groupsReference {
return groupsReference{
// data = make([]typ.Group, length)
// ┌──────────────────────┐
// │ Group 0 │
// │ ├── ctrlGroup │
// │ └── 8 个 Slot │
// ├──────────────────────┤
// │ Group 1 │
// │ ├── ctrlGroup │
// │ └── 8 个 Slot │
// ├──────────────────────┤
// │ Group 2 │
// │ ├── ctrlGroup │
// │ └── 8 个 Slot │
// ├──────────────────────┤
// │ Group 3 │
// │ ├── ctrlGroup │
// │ └── 8 个 Slot │
// └──────────────────────┘
// 这里创建 groups,但是里面的 slot 没有初始化成 Empty,需要手动设置
data: newarray(typ.Group, int(length)),
// length - 1
lengthMask: length - 1,
}
}
最后一个方法,group 方法返回第 i 个 group 的起始位置。
go
// group 返回第 i 个 group 的起始偏移量,groups 在内存中也是连续的
func (g *groupsReference) group(typ *abi.MapType, i uint64) groupReference {
offset := uintptr(i) * typ.GroupSize
return groupReference{
data: unsafe.Pointer(uintptr(g.data) + offset),
}
}
3. 小结
这篇文章简单学习了 group 里面的源码,下一篇文章就来看下 Map 整体的源码介绍。
如有错误,欢迎指出!!!