Go语言学习笔记

Go语言源码阅读与原理分析

Go的编译

数据结构

数组

声明

1
2
[3]{1, 2, 3}
[...]{1, 2, 3}语法糖

第二种编译器会进行转换成第一种

上限推导

语句转换

由字面量(具体整数,浮点数,字符串)组成的数组,根据长度进行优化

  • 元素数量<=4,元素放在栈上
  • >4时,元素保存在静态区,运行时取出

元素数量<=4时,简化为赋值表达式

1
2
3
4
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

review

编译之后的二进制文件包含:栈,堆,数据段,代码段

堆栈为动态区域,数据段和代码段为静态区域

栈:编译器自动分配释放,存放参数值,局部变量等

堆:程序动态申请的内存,malloc,用链表实现

代码区:函数体的二进制代码

数据段:包含

  1. 只读数据段 const
  2. 已初始化的读写数据段 初始化的全局变量,初始化的静态局部变量static
  3. 未初始化段 未初始化的全局变量和静态变量

当数组元素个数大于四个时

获取一个唯一的staticname,在静态存储区进行初始化,之后再拷贝到栈上

访问和赋值

编译器的静态类型检查时检测数组越界,索引是否为非负整数,索引越界

使用变量作为索引时,无法编译检查,需要运行时阻止,出发panic

发现数组切片字符串越界时通过运行时的runtime.panicIndexruntime.goPanicIndex触发panic

下标没有越界时,编译器获取数组的内存地址和访问下标,计算出目标地址,使用Load将元素加载到内存中

编译时插入运行时越界检查函数

赋值时先确定目标元素地址,使用Store指令将数据存入地址,在编译阶段而不是运行时

切片

编译时确定类型,存储在Extra字段

数据结构

1
2
3
Data
Len
Cap

切片只在运行时确定内容

初始化

切片slice[l:r]

调用SliceMake函数,参数为 元素类型,数组指针,切片大小和容量

这样初始化的切片创建了指向原切片的结构体

字面量

编译时:

对字面量数组做大小推断,初始化为数组

创建一个数组指针,指向静态数组

使用[:]通过指针创建切片

make

make([]int, len, cap) 会做参数校验,cap >= len

  1. 判断切片大小和容量是否足够
  2. 切片是否发生了逃逸,最终在堆上初始化

切片太大时也会在堆上初始化,使用运行时makeslice

makeslice会在堆上申请连续的内存

可能的运行时错误:

  1. 内存空间大小发生了溢出
  2. 申请内存大于最大可分配内存
  3. 传入的len<0 or len > cap

访问

对len和cap的访问会在编译时替换为常量

使用index获取元素会直接转换为对地址的访问

append和扩容

如果append之后不需要赋值给原有变量:

判断append之后的大小和容量触发扩容

如果append之后需要赋值给原有变量:

append后的切片覆盖原切片,编译器优化为不发生拷贝,直接操作原切片

growslice

先确定新切片容量,扩容策略:

  1. 如果期望容量大于当前的两倍,就会使用期望容量
  2. 当前切片长度小于1024会将容量翻倍
  3. 如果当前的切片长度大于1024每次增加25%的容量,直到新容量大于期望容量

扩容之后进行内存对齐,提高内存分配效率,减少碎片

使用预制的内存大小数组向上取整,然后通过该内存大小重新计算cap

对于非指针切片,将原数组内容拷贝至新内存

growslice返回的是一个新的切片,都是新的 slice(p, len, newcap)

copy

copy(a,b)

编译时会直接使用memmove拷贝到内存

运行时会直接进行合法性检查

大切片性能开销比较大

哈希

解决冲突

开放寻址

从index处向后寻找空闲位置,读取会从index处向后匹配相等元素

装载因子=元素数量/数组大小

装载因子增大,线性探测法的平均用时增加,最坏到On

拉链法

使用链表数组,每个数组是一个桶,通过index访问

装载因子=元素数量/桶数量

一般情况下不超过1,装载因子太大会触发扩容

struct

runtime hmap

保存对数,桶的数量都是2的倍数

包含runtime bmap,能存储8个键值对,超过8个时会使用extra.nextOverflow中的溢出桶存

bmap中存储key哈希的高八位tophash uint8

初始化

字面量

当哈希表的元素<=25个时,将初始化转化为

1
2
3
4
5
6
7
8
hash := map[string]int {
    "1": 1,
    "2": 2,
}
to
hash := make(map[string]int, 3)
hash["1"] = 1
hash["2"] = 2

超过时会转换为两个切片循环加入hash

运行时

当桶的数量小于$2^4$,不创建溢出桶

否则创建$2^{B-4}$个溢出桶

读写

遍历使用for range

删除delete(hash, key)

bmap的实际存储是tophash为一个连续的空间,keys,values 。。。

访问时限获取哈希值,再获取哈希的高8位

通过哈希的最低几位获取桶序号

这里因为哈希计算出来并不在桶范围内,在二进制中体现出来是取高几位和低几位,低几位就是取模之后的值,因此可以有效避免桶中有大量重复tophash

在bmap中先比较哈希高8位,加速访问

匹配成功会根据指针和偏移量获取key进行比较,匹配成功再获取value

尽量使用双值接收结果,防止实际的value是nil

写入时会遍历正常桶和溢出桶,溢出桶也包含tophash

如果桶满,创建新的桶护着在溢出桶中保存

获取存储地址后,将值拷贝到内存

扩容

在写入时,触发扩容

  1. 装载因子超过6.5
  2. 哈希使用了太多溢出桶,容易产生内存泄露

扩容非原子过程,扩容前判断是否正在扩容

溢出桶太多触发等量扩容,新桶保存数据,回收旧桶(大量的写入删除操作)

翻倍扩容,随着写操作增量进行,不会产生性能的巨大抖动,创建一组新桶和溢出桶,将原来的桶组设置到oldbuckets,溢出桶也设置到oldoverflow上

数据迁移发生在运行时 evacuate,对传入桶的元素再分配,每个旧桶元素分流到两个新桶

分流逻辑:原来通过取模得到的桶掩码为0b11,扩容翻倍之后将变为0b111,因此该数据被分流到3号和7号桶

当旧桶完全被分流后清除oldbuckets和oldoverflow,通过计数器控制

扩容期间访问时,若oldbuckets存在,并且旧桶没有被分流时会先去旧桶寻找

扩容期间写入赋值时,会触发增量拷贝,向新桶分流

删除

扩容期间删除,会分流桶中的元素,然后找到桶中的目标键值对删除

与写入类似

访问,写入删除都是运行时处理

字符串

只读的字节数组,使用连续空间

data和len

分配在只读的内存空间,修改需要和[]byte相互转换

  1. 拷贝内存到栈或者堆
  2. 将变量修改为[]byte然后修改字节数据
  3. 修改字节数组转回string
1
2
3
4
5
6
7
str := "hello\""
str := `
{
    "name" : "panda",
    "tags" : ["panda"]
}
`

赋值时使用scanner解析字符串成token流

strconv.Unquote去除引号

使用+拼接,拼接字符串的数量小于等于五个时,使用concatstring{2,3,4,5},否则使用runtime.concatstrings,传入数组切片,最终通过运行时过滤空字符串计算拼接后长度,如果非空字符串数量为1,并且不在栈上,则直接返回

拷贝到目标地址空间

类型转换有性能损失,需要拷贝数据

函数调用

c语言使用寄存器传参数,小于等于六个时使用寄存器,大于6个的参数使用栈传递,使用寄存器传递返回值,并且只使用一个寄存器,因此只能有一个返回值。

go语言使用栈传递参数和返回值,因此存在性能损失,但是可以支持多返回值,便于维护编译器,不需要考虑寄存器数量和命名。

参数传递方式

传值,基本类型,结构体,指针,对参数进行拷贝

0%