Go知识点
一些GO的知识点。
Go的知识点
编译相关
Go可以生成汇编,采用下列指令进行查看:
1 | go build -gcflags -S main.go |
Go可以生成汇编优化过程。通过GOSSAFUNC,可以生成html文件来交互查看。
SSA:静态单赋值。即每个变量只会被赋值一次,便于优化。同一变量名字会生成不同后缀来区分。
GO可以生成各种机器码,包括wasm。
TODO:待到学完编译原理在看。
数据结构
数组
只有类型和大小完全相同,才能认为是同一数据类型,可以用==比较。
【…】T{1,2,3}与【3】T{1,2,3}使用上完全相同,只是语法糖。
当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上。
当元素数量大于 4 个时,会将数组中的元素放到静态存储区初始化然后拷贝到栈上。
简单的越界错误可以由编译器发现,复杂的则需要运行时。运行时发现越界时候会panicIndex和runtime.goPanicIndex。运行时会插入代码,检查边界,越界则panic,通过则Load或Store。
切片
slice有data指针,len长度,cap容量。
切片初始化可以有make创建,字面量初始化,或者截取数组或者切片的一部分。
切片逃逸:1、如果函数外部没有引用,则优先放到栈中;2、如果函数外部存在引用,则必定放到堆中;
切片很小并且非发生逃逸,会在栈或静态存储区初始化。否则在堆区初始化。
cap足够时候append覆盖时候会优化情况。不够则会调用growslice扩容并且拷贝过去。扩容规则如下:
如果期望容量大于当前容量的两倍就会使用期望容量; 如果当前切片的长度小于 1024 就会将容量翻倍; 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
大致容量如此,接下来还需要进行对齐操作。
拷贝时候会memove,这比依次拷贝性能更好,耗费资源仍然较多。
哈希表
开放地址法:填空。
拉链法:挂上链表。
map底层为hmap,由若干个bmap桶组成。每个桶通常可以存8个元素。
通过哈希值低8位找到桶,在通过高八位对比tophash数组来查找。
分配时候若桶满则可以放入溢出桶,用overflow指针指过去。
元素少于25个则直接hash,多余25则创建两个数组存储键值,然后for哈希过去。
在make时候,则会检查内存是否足够,获取哈希种子,计算最少桶数量,创建保存桶数组。
仅获取value的访问采用mapaccess1,会计算哈希值,查找桶,查找tophash,并且访问。mapaccess2就多个返回值。
当装载因子超过6.5或使用太多溢出桶会触发扩容 。扩容将桶数量翻倍,通过growWork触发,访问时候访问旧桶,写入则分流过到新桶。
删除和赋值逻辑类似。
字符串
只读字符串,由data指针和len组成。
如果要修改,则要转换成为【】byte来进行。开销并不小。
逃逸分析
取地址或者逃出了函数,按照此原则分析变量分配方法
语言基础
函数调用
在c语言中,函数调用参数使用寄存器与栈实现。6个一下为寄存器。以上放入栈。
Go采用栈来入参和出参,简化了实现,不必考虑架构。
Go无论是基本类型,结构体,还是指针,都会传值拷贝。
接口
接口引入中间层,实现上下游解耦。在GO中,实现接口所有方法就算实现了接口。
interface{}不是任意类型。当我们转换成interface时候,类型就是interface。
函数接收者不是指针的时候,能通过结构体和指针使用方法。是指针的时候,只能通过指针使用方法。不能同时存在同名方法定义两次。
nil在转换成interface类型时候会包含以前的类型信息,因此转换后不是nil。
对于空interface而言,结构体为eface,不含方法,之包含类型与数据指针。_type类型中有数据大小,哈希值来快速确定类型是否相当,equal判断多个对象是否相等。
非空interface包含itab指针与数据指针。itab中包含对上述_type的哈希,来确认目标类型与具体类型是否相等。有fun动态大小数组,当成虚函数表,存储函数指针。同时包含原类型type和接口类型interfacetype。
非指针变量转换为接口会拷贝到堆上。
断言switch a.(type)时候会检查哈希值。
反射
TypeOf接受空接口参数,转换为emptyInterface类型,并且获取typ并返回。
ValueOf则是先将其逃逸到堆上,热爱华南虎使用unpackEface将传进来的转换为emptyInterface结构体,包装成Value结构体并且返回。
更新变量时候,会检查变量是否对外公开。然后调用assignTo返回新反射对象,并且覆盖原始反射变量。
eface用于运行时,emptyInterface用于反射。
for range
在for _,v:=range arr时候,系统会创建一个变量v,将数组值在迭代中覆盖过去,这导致v的地址在循环中不变。
使用for循环清空数组,切片,哈希表时候,会编译成arrayClear,加速过程,开销并不大。
range循环不会永动机,因为最开始会调用len来获取终止条件。
哈希表遍历:随机选一个正常桶开始,遍历桶内,桶外溢出桶,再按顺序来。
select
select与switch类似,但状态只能是chan收发状态。
多个条件都满足时候,switch会随机执行,避免饥饿状态。
select的case用结构体表示
select不存在case直接阻塞,只有一个case则变成单if,一个case一个default会改写成if,else,默认情况下会将case转换为结构体,调用selectgo函数获取一个可行的结构体,使用一连串if观察是哪个被选中了。
轮询会随机开始,加锁顺序则是按地址排序。
调用select先确认轮询顺序加锁顺序,之后如果能立即执行则立即执行并返回,不能则创建sudog结构体,加入相关的收发队列,挂起等唤醒,唤醒了则遍历去找。
defer
defer会在函数结束执行,而不是代码作用域结束。
defer参数的值是在调用时候计算的。
defer采用延迟链表,最早只有堆上分配,后期有栈上分配,开放编码优化。
根据defer数量和return数量判断是否开放编码优化。直接插入到函数返回前。
panic recover
panic时候,会放到panic链表最前面,然后获取defer链表,执行,最后panic。
程序的恢复由gopanic执行。取出栈顶指针和pc,执行recovery函数,recovery则跳回去。
make new
make需要判断类型,new则只用初始化指针,申请空间。
context
基本用法如下:
1 | func main() { |
chan
chan结构体内包含队列元素个数,循环队列长度,数据指针,发送waitq,接收waitq。
waitq表示双向链表,包含向前的sudog和向后的sudog。
发送前会先上锁,再判断是否closed。
直接发送:先拷贝到目标地址,再标记接收goroutine,放进处理器runnext等待执行。
带缓冲区:计算下一个可以存储数据的地方,再将数据拷贝到缓冲区,增加索引与计时器。
缓冲区为循环数组。
阻塞式发送:则先获取发送数据的goroutine,再获取sudog设置阻塞发送相关信息,加入发送等待队列,沉睡并等待唤醒,唤醒后首尾。
接收:如果不在缓冲区且就绪,则直接拷贝。在缓冲区,则拷贝并且移动缓冲区指针。
chan为空则挂起,关闭则检查缓冲区,无数据则返回。如果发送队列存在挂起的,则从缓冲区拷贝到接收,再将发送挂起的拷贝进缓冲区。缓冲区存在数据则直接读,否则挂起。
GMP模型
G表示goroutine,M表示线程,P表示处理器。
G类似线程,包含自己的内存,栈,寄存器状态,再调度器保存或者恢复上下文的时候用到。
M操作系统线程,GOMAXPROCS控制活跃数量,默认为CPU核数,减少系统调度开销。
P提供上下文环境,可以调度线程上等待队列。
调度时间点:主动挂起,系统调用,协作式调度,系统监控。
垃圾收集
三色标记法:黑色,白色与灰色。
- 黑色代表活跃的对象,已经被访问过,并且本对象引用的其他对象也被标记过。
- 灰色代表活跃的对象,已经被访问,但存在引用的其他对象未被访问。
- 白色代表潜在的垃圾,未被访问过。
当开始运行时候,根对象被标记为灰色对象,然后之后的程序只从灰色对象向外扩展。类似BFS。当灰色集合不存在的时候,遍历结束,回收白色的垃圾。
GC是一个过程,中途可能改变对象的引用指向,可能会增加,可能会删除,因此需要遵守两个不变性之一:强三色不变性-黑色不会指向白色;弱三色不变性-黑色指向的白色的可达路径上一定有灰色(白色肯定能被灰色扩展到)
引入写屏障技术,在写进内存前做一些干涉,保证gc的正确性即可。存在两种写屏障:Dijkstra插入写屏障与Yuasa删除写屏障。
Dijkstra插入写屏障:在黑色需要指向白色时候,将白色染成灰色即可。但由于栈上的对象会被认为成根对象,因此要么为栈上的对象添加写屏障,要么在标记结束后再次扫描栈。
Yuasa删除写屏障:在删除对象的时候,将被删除的对象标记为灰色。这样保证了弱三色不变性,但回收精度低,有的需要下一轮才能回收。