幼麟实验室笔记
Golang底层知识
目录
1 学习前的介绍
通过幼麟实验室的学习让我更加深刻的去理解Golang的底层原理,看完最深刻的感受就是脑子好痒,非常干,全是干货。自己做的笔记整理出来,方便复习和阅读。
2 string
2.1 Unicode字符编码
变长编码
编号 | 编码模板 | 最高位固定标识位 |
---|---|---|
[0, 127] | 0??????? | 0 |
[128, 2047] | 110????? 10?????? | 110 和 10 |
[2048, 65535] | 1110???? 10?????? 10?????? | 1110 和 10 和 10 |
例如
界 编号:30028——->二进制(01110101 01001100)
将其填如对应的?位置——>得到界UTF-8编码:11100111 10010101 10001100
2.2 string的数据结构
- golang不用特定标识符表示字符串的结尾。
- golang采用string结构体来共同描述一个字符串。
- data是指向字符串开始位置的指针。
- len表示字符串的字节个数(非字符个数),因为golang采用utf8的变长编码方式,故不能采用字符个数。
2.3 string的底层细节
- golang将string类型分配到只读内存段,因此不能通过下标的方式修改。
- s2的起始地址是第3个字节,字节数目是8(起始地址到s1结尾字节数目)
- 多个string变量可以共用同一个字符串的某个部分。(如上图s2:如果s1修改eggo为egoo,那么s2也会受影响)
- 可以给变量整体附新值
s1="Hello"
,那么s1就会指向新的地址。 - 可转为字节切片对字符串内容进行修改,不过改动的是拷贝后的字符串(上图bs切片)
3 slice
3.1 slice结构
slice分三部分:【存哪里】【存了多少元素】【可以存多少元素】(data, len, cap)
var a []int
:变量a —> (data=nil, 0, 0)var a = make([]int, 3, 5)
变量—>(data=起始地址, 3, 5)new一个slice
3.2 slice扩容机制
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
扩容之后的数组一定是新的么
- 情况一:切片的cap还够用,则扩容后还是原来的底层数组。
- 情况二:切片的cap不够用,则扩容后会开辟新的数组,将原来的值拷贝后再扩容,此时的底层数组是新开辟的而不是原来的。
如上图中扩容后cap=5后发生什么(假如机器是64位)
- 内存管理模块会提前向操作系统申请一批不同规格的内存地址如(8, 16, 32, 48, 64, 80, 96…)
- 预估申请的内存会匹配到合适规格的内存,5 * 8 = 40byte, 那么就会匹配到规格为48字节的内存。
4 内存对齐
4.1 地址总线和数据总线
- CPU是通过地址总线来对存储单元进行寻址的,8根地址总线[0,255]可寻址256Byte内存,32根地址总线[0,2^32-1]可寻址4G内存。
- CPU与内存或者其他器件之间的数据传输时通过数据总线来进行的。数据总线的宽度决定了CPU和外界的数据传输速度。8根数据总线一次即可传送8位二进制数据(1个字节);16位即可一次传送两个字节。每次操作的字节数称为机器字长。
4.2 内存对齐
现代计算机中内存空间都是按照字节(byte)进行划分的,编译器会把各种类型数据安排合适的地址,并占用合适的长度,这种就称为内存对齐,内存对齐是指首地址对齐。
4.2.1 为什么要内存对齐
- 有些
CPU
只能在特定地址访问数据,不同硬件平台具有差异性,在编译时将分配的内存进行对齐,这就具有平台可以移植性了。 CPU
访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问,内存对齐后可以提升性能。
现代计算机中内存空间都是按照字节(byte) 进行划分的,编译器会把各种类型数据安排合适的地址,并占用合适的长度,这种就称为内存对齐,内存对齐是指首地址对齐。
4.2.2 举个栗子
32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。这么设计的目的,是减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。比如同样读取 8 个字节的数据,一次读取 4 个字节那么只需要读取 2 次。
变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。
4.2.3 对齐边界
- 每种类型的对齐值就是它的对齐边界。
- 内存对齐要求数据存储地址及占用的字节数都要是它对齐边界的倍数。
取类型大小与平台最大对齐边界中较小的值。
4.3 结构体内存对齐
- 结构体对齐边界是成员中最大的对齐边界
- 结构体占用字节数需要是类型对齐边界的倍数
5 map
5.1 map的结构
Go语言中map是散列表的引用。散列表可以用来存储键值对元素。
map类型是map[k]v,其中k是字典的键,v是字典中值对应的数据类型。
<table><tbody><tr><td class=”gutter”><pre><span class=”line”>1</span><br></pre></td><td class=”code”><pre><span class=”line”><span class=”keyword”>var</span> a <span class=”keyword”>map</span>[<span class=”type”>string</span>]<span class=”type”>string</span></span><br></pre></td></tr></tbody></table>
map类型的变量本质上是个指针,这些键值对实际上是通过哈希表来存储的。
5.1.1 Map类型
Go语言中Map类型的底层实现就是哈希表。实现了快速查找、添加、删除的功能。
map类型变量本质上是一个指针,指向hmap结构体。
5.1.2 bmap的结构
- 8个tophash:对应hash值的高8位
- 8个key在一起
- 8个value在一起
- bmap类型的指针,指向溢出桶
5.1.3 溢出桶
哈希表要分配的桶数目大于2^4时,使用溢出桶的概率较大,会预分配2^(B-4)个溢出桶备用;这些溢出桶与常规桶内存中是连续的,只是前2^4作为常规桶,后面作为溢出桶。
mapextra:溢出桶信息
- overflow:已使用的溢出桶地址
- oldoverflow:旧桶使用的溢出桶地址
- nextoverflow:下个空闲溢出桶地址
5.2 常用方法
- 取模法:hash % m
- 与运算法:hash & (m-1) ,注意m需要是2的整数次幂,否则会出现空桶(绝对不会选中)
5.3 哈希冲突
- 开放地址法: 当冲突时,查找下一个位置继续判断是否冲突,直到无冲突为止
- 拉链法:冲突时在位置上再建立一条链,存储相应映射到该位置的数据,查找时,也是对于链上元素进行遍历,删除时只需要打上标记,通常不实际进行删除。
哈希冲突会影响哈希表的读写效率,解决方法:
- 选择散列均匀的哈希函数
- 适时进行扩容
5.4 扩容
5.4.1 何时扩容
哈希表中存储的键值对数量和桶数量的比值会作为判断哈希表是否需要扩容的依据,这个依据叫做负载因子(Load Factor)通常需要设定一个触发扩容的最大负载因子。Go语言默认负载因子是6.5。
负载因子 = 填入哈希表中的元素个数 / 哈希表的长度
5.4.2 如何扩容
渐进式扩容:把键值对迁移时间分摊到多次哈希表操作中的方式。可以避免一次性扩容带来的性能瞬时抖动。
翻倍扩容:Go语言默认负载因子是6.5,超过这个数会触发翻倍扩容
等量扩容:负载因子没有超标,但溢出桶较多,会触发等量扩容。
常规桶数量 = 2 的 B 次方
解释:若溢出桶过多也会触发 map 的扩容, 这是基于这样的考虑, 向 map 中插入大量的元素, 哈希桶将逐渐被填满, 这个过程中也可能创建了一些溢出桶, 但此时装载因子并没有超过设定的阈值, 然后对这些 map 做删除操作, 删除元素之后, map 中的元素数目变少, 使得装载因子降低, 而后又重复上述的过程, 最终使得整体的装载因子不大, 但整个 map 中存在了大量的溢出桶, 因此当溢出桶数目过多时, 即便没有达到装载因子 6.5 的阈值也会触发扩容,其目的是整理map桶。
6 类型系统
方法调用
type T struct { |
方法调用本质上是函数调用。
自定义一个结构体类型T,并给它关联一个方法F1。当调用方法F1时,实际上就是T.F1(t)
。方法的接受者作为函数的第一个入参传入。
6.1 如何动态获取数据类型信息
Go内置类型
给内置类型定义方法是不允许的
- bool
- int、int8、int16、int32、int64、uint、uint8(byte)、uint16、uint32、uint64
- float32、float64
- complex64、complex128
- string
- rune
- error
- slice、map、func
自定义类型
接口类型是无效的方法接受者,因此不能给接口类型关联方法。(例如下面的Test方法)
type MyInt int |
但是不管内置类型还是自定义类型,都有对应的类型描述信息。而且每种类型元数据都是全局唯一的,这些类型元数据共同构成了Go语言的类型系统。
类型元数据信息
runtime._type保存了类型的元数据信息,并且作为每个类型元数据的Header。
type _type struct { |
在_type之后保存的是各种类型额外需要的描述信息。
例如slice的类型元数据,在_type后有一个__type,指向其存储的元素的类型元数据。[]slice的类型元数据中的__type就指向string类型的元数据(stringtype)
type slicetype struct { |
自定义类型
自定义类型除了有_type和*_type之外,还有一个uncommontype结构体。
type uncommontype struct { |
举例:
自定义myslice类型,并为它关联两个方法。
type myslice []string |
myslice的类型元数据如下:
首先是slice的类型元数据,后面是uncommontype记录的信息。假设uncommontype的地址为addrA,加上moff字节的偏移量,就是myslice关联的方法元数据数组。
6.2 类型定义和类型别名
类型定义
类型定义会基于已有类型创建新类型,MyType会自立门户拥有自己的元数据,即使MyType的类型元数据和int32的类型元数据没有任何改变。
类型别名
MyType2和int32会关联到同一个类型元数据。rune和int32就是这样的关系。
7 接口
空接口
// 空接口类型结构体 |
空接口类型可以接收任意类型的数据,它只要记录这个数据是什么类型,这个数据在哪里即可。
_type指向接口的动态类型元数据,data指向接口的动态值。
举例
var e interface{} |
一个空接口类型的变量e,在被赋值前,_type和data都是nil。现在有一个*os.File
类型的变量f,将它赋值给e。因为f本来就是指针类型,所以data就等于f,_type指向*os.File
的类型元数据。
*os.File
作为自定义类型,除了头部(_type),还有uncommontype信息。通过uncommontype,就可以获取*os.File
中的方法列表。
非空接口
type iface struct { |
非空接口就是有方法列表的接口类型。一个变量想要赋值给非空接口类型,必须要实现该接口要求的所有方法。
与空接口类型一样,data字段也指向接口的动态值,tab字段保存接口要求的方法列表和接口动态类型信息。
type itab struct { |
// interface的类型元数据 |
举例
var rw io.ReadWriter |
声明一个io.ReadWriter类型的接口变量rw,未被赋值前,tab和data都是nil。然后将一个*os.File
类型的变量f赋值给rw。
此时rw的动态值(data)就是f,tab会指向一个itab结构体,它的接口类型是io.ReadWriter,动态类型为*os.File
。
itab这里的fun,会从动态类型元数据中拷贝接口要求的那些方法的地址,以便rw快速定位到方法,而不需要去类型元数据那里查找。
itab复用
Go语言会把用到的itab结构体缓存起来,并且以接口类型和动态类型组成key,以itab为value,构建一个哈希表。
const itabInitSize = 512 |
当创建非空接口时并对它实现时,会首先从哈希表中查询,如果有,直接返回。如果没有,创建新的itab后放回哈希表中。
本文转自 https://juejin.cn/post/7204501231262695480,如有侵权,请联系删除。
参考链接
幼麟实验室bilibili:https://space.bilibili.com/567195437/?spm_id_from=333.999.0.0
本文转自 https://jpcly.cn/archives/7c9d44f8,如有侵权,请联系删除。