golang学习笔记
语法
- 内层词法域可以屏蔽外层词法域中的变量
- 花括号为显式词法域
- for,switch,if的条件部分会创建隐式词法域
rune=int32
byte=uint8
运算符优先级
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
格式化
符号 | 功能 | 例 |
---|---|---|
%[1]o | 使用第一个操作数 | fmt.Printf("%d %[1]o %#[1]o\n", o) |
%#o | 用%o、%x或%X输出时生成0、0x或0X前缀 用Go语言语法打印值 |
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) |
%q | 打印带单引号字符,无损打印 | |
%c | 打印字符 | |
% x | 在每个十六进制数字前加入一个空格 | |
%t | 打印布尔类型 | |
%T | 打印数据类型 |
- 浮点数中,NaN、正无穷大和负无穷大都不是唯一的
字符串处理
bytes
- 针对[]byte类型提供
strings
- 字符串查询,替换,比较,截断,拆分,合并
strconv
- 布尔型,整型,浮点型和对应字符串的相互转换
整数转字符串Itoa(Int to ASCII)
FormatInt(int, base)
FormatUint(int, base)
Atoi(s)
ParseInt(s, base, bit)
unicode
- 字符分类,数字,大小写判断,转换
常量
- 表达式值计算在编译期,而非运行期
代码风格
- 正常执行的语句不要代码缩进
- 娜扎铁律:注释,日志,测试
- 使用copy()复制切片和map
- 返回切片或map,创建新变量进行遍历复制并返回
- 使用defer管理文件和锁等资源(除非函数执行时间为纳秒级)
map
- map并发读写,panic无法recover
slice扩容
容量 | 扩容大小 | 扩容比例 |
---|---|---|
2 | 1 | 1 |
4 | 2 | 1 |
8 | 4 | 1 |
16 | 8 | 1 |
32 | 16 | 1 |
64 | 32 | 1 |
128 | 64 | 1 |
256 | 128 | 1 |
512 | 256 | 1 |
1024 | 512 | 1 |
1280 | 256 | 0.25 |
1696 | 416 | 0.325 |
2304 | 608 | 0.35849056 |
3072 | 768 | 0.33333334 |
4096 | 1024 | 0.33333334 |
5120 | 1024 | 0.25 |
7168 | 2048 | 0.4 |
9216 | 2048 | 0.2857143 |
12288 | 3072 | 0.33333334 |
15360 | 3072 | 0.25 |
19456 | 4096 | 0.26666668 |
24576 | 5120 | 0.2631579 |
30720 | 6144 | 0.25 |
38912 | 8192 | 0.26666668 |
49152 | 10240 | 0.2631579 |
61440 | 12288 | 0.25 |
76800 | 15360 | 0.25 |
96256 | 19456 | 0.25333333 |
120832 | 24576 | 0.25531915 |
151552 | 30720 | 0.2542373 |
189440 | 37888 | 0.25 |
237568 | 48128 | 0.25405404 |
296960 | 59392 | 0.25 |
371712 | 74752 | 0.25172412 |
464896 | 93184 | 0.2506887 |
581632 | 116736 | 0.25110132 |
727040 | 145408 | 0.25 |
909312 | 182272 | 0.25070423 |
1136640 | 227328 | 0.25 |
1421312 | 284672 | 0.25045046 |
1776640 | 355328 | 0.25 |
2221056 | 444416 | 0.2501441 |
2777088 | 556032 | 0.2503458 |
3471360 | 694272 | 0.25 |
4339712 | 868352 | 0.2501475 |
5425152 | 1085440 | 0.250118 |
6781952 | 1356800 | 0.25009438 |
8477696 | 1695744 | 0.25003776 |
10597376 | 2119680 | 0.2500302 |
JSON
type struct xx{
Color bool `json:"color,omitempty"`
}
iota
- iota按行递增
封装
- 调用方只需关注少量值
- 隐藏实现细节,使开发者在不破坏
api
的情况下自由开发(bytes.Buffer
预分配空间) - 阻止调用者对对象内布置的修改
CSP
顺序通信进程(communicating sequential processes)
channel
不要通过共享内存进行通信,而应通过通信进行内存共享
使用
- go有锁数据结构
- 有capacity,异步非阻塞
- 无capacity,同步通信
- 再次关闭已关闭channel会panic
- 使用
for elem := range ch
,在channel关闭时自动退出循环 - 向已关闭的channel中发送数据会panic
- 关闭挂在goroutine阻塞的channel会panic
- close一个channel会唤醒所有等待该channel的其它所有G,并使其进入Grunnable状态,这些Goroutine发现该channel已closed,会panic
- 可以从已关闭channel中接受数据
- 使用
x, ok := <-ch
中ok的值判断channel是否关闭,以及x为数据或零值 - 关闭nil channel(使用
var a chan int
创建的channel)会panic - nil channel发送数据会永远阻塞
happens before: 接受者接收数据发生在唤醒发送者goroutine之前
关闭原则
- 一个sender,多个receiver,由sender关闭
- 多个sender,借助额外channel做信号广播
- 不要关闭已关闭通道,或向已关闭通道发送值
检查通道关闭
package main
import "fmt"
type T int
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}
发送
+++++++++++++++
| buf | -----> | x | x | x | x | x |
| sendx |
| recvx |
| sendq | -----> +++++++++
| recvq | | g | ---> G1
| closed | | elem | ---> 6
| lock | | ... |
+++++++++++++++ +++++++++
-
buf满时,用sudog包裹g和要发送的数据,入队sendq,并将当前gorutine进行gopark(m解除当前的g, m重新进入调度循环, g没有进入调度队列)
-
出现新接收方时,sendq出队,从buf拷贝队头,从sender拷贝到队尾,goready(放入调度队列,等待被调度)
-
读取空channel时,用sudog包裹g和要发送的数据,入队recvq,gopark
关闭channel
- 加锁
- closed=1
- ready所有sendq和recvq
- 解锁
读取已关闭channel
- sendq和recvq为nil
- buf可能不为空
- 为空则清零reader读取位置
- 不为空则继续读buf
select
type hselect struct {
// 总 case 数
tcase uint16 // total count of scase[]
// 当前已初始化填充的 case
ncase uint16 // currently filled scase[]
// 轮询顺序
pollorder *uint16 // case poll order
// case 的 channel 的加锁顺序
lockorder *uint16 // channel lock order
// case 数组,为了节省一个指针的 8 个字节搞成这样的结构
// 实际上要访问后面的值,还是需要进行指针移动
// 指针移动使用 runtime 内部的 add 函数
scase [1]scase // one per case (in order of appearance)
}
- 如果 select 中实际只操作一个 channel,写成 for range 形式:
for d := range ch {
// do some happy things with d
}
- 如果 select 中需要监听多个 channel,并且这些 channel 可能被关闭,写双返回值的 channel 取值表达式:
for {
select {
case d, ok := <-ch1:
if !ok {
break outer
}
case d, ok := <-ch2:
if !ok {
break outer
}
}
}
select不支持throughfall,随机选择case使用堆实现
panic
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // 指向上一个panic
recovered bool // recover标志
aborted bool // the panic was aborted
}
- map高并发读写时,程序直接崩溃,会panic,但无法用recover捕获
- panic先执行当前goroutine上已有的derfer任务(仅限当前goroutine),然后检查recovered标记
- 如果为true,退出panic流程,程序恢复正常;
- 如果为false,则退出程序
- panic退出时会打印调用栈,最终调用exit(-2)退出整个进程
recover
- 只在defer上下文中生效
- 不在defer上下文中调用,直接返回nil
recover流程
- 检查current gorountine是否在panic流程中,如果不在直接返回nil
- 执行完recove后,调用recovery
- 在recovery中将调用recover的defer函数设置到当前goroutine中的sched中,并将ret设置为1
接口
interface结构
// iface描述接口包含方法
type iface struct {
tab *itab // 接口类型
data unsafe.Pointer // 具体值
}
type itab struct {
inter *interfacetype // 接口类型
_type *_type // 实体类型,内存对齐方式
fun [1]uintptr // 数组,保存实际方法地址(第一个方法)
}
type interfacetype struct {
typ _type // 描述 Go 语言中各种数据类型的结构体
pkgpath name // 接口包名
mhdr []imethod // 接口定义函数列表
}
// eface不包含任何方法的空接口
type eface struct {
_type *_type
data unsafe.Pointer
}
- _type
type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等等等等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
type arraytype struct {
typ _type
elem *_type
slice *_type
len uintptr
}
type chantype struct {
typ _type
elem *_type
dir uintptr
}
type slicetype struct {
typ _type
elem *_type
}
type structtype struct {
typ _type
pkgPath name
fields []structfield
}
判断myWriter是否实现io.Writer接口
var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}
-
interface保存一对数据
变量实际的值
变量的静态类型
类型转换
<结果类型> := <目标类型> ( <表达式> )
类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言
- v.(type)在switch中不支持fallthrough
- 类型断言针对接口变量进行操作
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法
> 接口转换- 当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
- 某类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为
O(mn)
,Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为O(m+n)
。
接口值
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
定义值初始化
将
*os.File
赋值给w
使用w.Write([]byte("hello"))
效果和os.Stdout.Write([]byte("hello"))
一样
将
*bytes.Bufffer
赋值给w
将nil赋值给w
w将恢复到和他定义时相同的状态
接口值可以持有非常大的动态值
var x interface{} = time.Now()
接口动态值的动态类型必须可比较才能进行比较,否则会导致panic(如Slice)
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
- 值和类型都为nil的接口为nil接口
sort.Interface
type Interface interface {
Len() int
Less(i, j int) bool // i, j are indices of sequence elements
Swap(i, j int)
}
// 使用组合实现Reverse
type reverse struct{ Interface } // that is, sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
子类多态/非参数多态
内存
内存布局
-
在包级别声明的变量会在main入口函数执行前完成初始化
-
内存块:元数据(大小,是否使用,下块内存地址),用户数据,对齐字段
更快分配内存
- 虚拟内存:多线程共享内存
- 线程预分配缓存进行一次系统调用,后续线程申请小内存从缓存分配属于用户态执行
- 多个线程同时申请小内存时,从各自缓存分配,无需加锁
TCMalloc(Thread Cache Malloc)
- Page:为系统虚拟内存页的倍数,x64为8KB
- Span:一组连续的Page,TCMalloc中内存管理的基本单位
- ThreadCache:每个线程有各自的Cache,每个Cache有多个空闲内存块链表,每个内存块链表具有相同大小内存块(访问无需加锁)
- CentralCache:线程共享缓存(访问需要加锁),ThreadCache无缓存时从CentralCache中取,足够时放回CentralCache
- PageHeap:堆内存,若干Span链表,每个链表保存Page数量相同的Span,CentralCache无缓存时从PageHeap中取,将Span拆分成内存块,添加到对应大小的链表。CentralCache足够时则放回PageHeap
Go内存管理
内存管理基本对象
-
Page:x64为8KB
-
mspan:内存管理基本单位,一组连续Page
-
mcache:保存各种大小mspan(无锁访问),每个P拥有1个mcache
-
mcentral:所有线程共享缓存,需加锁访问,每个级别mspan有2个链表(1个包含指针对象,1个不包含指针对象)
-
mheap:堆内存,将申请到的内存页组织成Span,然后组织成树结构(非链表)
大小转换
- Tiny:大小在1Byte~16Byte之间,不包含指针的对象
- 小对象:16Byte~32KB
- 大对象:>32KB
寻找mspan
- 计算对象所需内存大小size
- 根据size到size class映射,计算所需的size class
- 根据size class和对象是否包含指针计算span class
- span class指向span
mcentral
- nonempty:链表内尚有空闲object的span,mcache释放span时加入链表
- empty:链表内没有空闲object,或已被cache取走的span,span交给mcache时加入链表
- mcache向mcentral申请span时,优先取已有资源,即使需要执行清理操作,先从nonempty中查找,没有再从empty中找。只有现有资源无法满足时,才去heap中获取span,并重新切分成object链表。
mheap的span管理
- 2棵二叉排序树,按照span的page数量排序
- free:空闲并且非垃圾回收的span
- scav:空闲并且垃圾回收的span
- arenas:heap mrena映射
mcentral向mheap申请span
- mcentral向mheap提供内存页数和span class
- mheap先从free中搜索,如果未找到,再从scav中搜索,如果未找到,向OS申请,然后在搜索free和scav
- 如果找到的span比需要的span大,将span分割为2个,其中1个满足需求大小,提供给mcentral,另1个加入free中
mheap申请内存
- mheap内存不足时,向OS申请内存
- 将申请到的内存保存到span
- 将span插入free
mheap初始化
1.创建对象规格大小对照表。
2.计算相关区域大小,并尝试从某个指定位置开始保留地址空间。
3.在heap里保存区域信息,包括起始位置和大小。
4.初始化heap其他属性。
大对象分配
- 大对象内存分配流程与mcentral向mheap申请内存类似
- mheap会记录大对象统计信息
Go垃圾回收和内存释放
- 垃圾回收收集不再使用的span,并将span释放给OS(标记已不再使用,可以回收。是否回收,由内核决定).
- 以span为基本单位,通过对比bitmap中的扫描标记,逐步将object收归原span,最终上交central或heap
- 遍历span,收集不可达的objcet合并到freelist,如果span已回收全部objcet,将该span还给heap
Go栈内存
- goroutine初始大小2KB.
- 动态扩大为原来两倍(runtime.morestack).
- 栈缩容发生在垃圾回收期间,函数使用空间小于当前空间1/4时,执行缩容,收缩为现在的1/2.
- 关闭缩容:GODEBUG=gcshrinkstackoff=1.
内存分配器初始化
保留虚拟地址空间(不分配内存)被划分成三个区域:
页所属span指针数组 GC标记位图 用户内存分配区域
+-------------------+--------------------+----------------------------------------------+
|spans 512MB |bitmap 32GB |arena 512GB |
+-------------------+--------------------+----------------------------------------------+
spans_mapped bitmap_mapped arena_start arena_used arena_end
- areana:向OS申请内存
- bitmap:为每个对象提供4个标记位,保存指针,GC标记
- spans:创建时按页填充,回收object时,将其地址按页对齐可找到对应所属span
newobject内存分配
- 获取当前线程绑定的cache
- 是否为<16Byte的且无需扫描的tiny对象?
- 如果是,
- 先置偏移量为16,对齐判断是否为更小的对象
- 修改偏移量,剩余空间足够?
- 足够,则调整偏移量,为下次分配做好准备,并返回
- 不够,
- 从cache中获取新的tiny块(从sizeclass=2的span.freelist中获取一个16Byte的object)
- 如果没有可用的object,则从central中获取新的span,然后提取tiny块
- 调整span.freelist链表,增加使用计数
- 初始化(零值)tiny块
- 如果旧块剩余空间小于新块大小,则用新块替换旧块
- 如果是,
- 是否为>16Byte <32KB的小对象?
- 是
- 查表确定对象的sizeclass
- 从对应的span.freelist中提取object
- 如果没有可用object,从central中获取新的span,并重新提取object
- 调整span.freelist链表,增加使用计数
- 初始化(零值)
- 是
- >32KB的大对象?
- 直接从heap中分配span
- 在bitmap中做标记
- 检查触发条件,启动垃圾回收
内存扩张
- 计算所需页数(size>>_PageShift) _PageShift=1<<13(KB)
Tips
- 使用缓存提高效率.
- 以空间换时间,提高内存管理效率(对象大小转换表).
- 物理内存分配在写操作导致缺页异常调度时发生,而且是按页提供的.
GMP
- G:Goroutine。包括执行的函数指令及参数;G保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。
- M:线程。当指定了线程栈,则M.stack→G.stack,M的PC寄存器指向G提供的函数,然后去执行。
- P:Processor。当P有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务,包含运行goroutine的资源,M必须和一个P关联才能运行G,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
+------------------sysmon---------------//--------+
| |
| |
+---+ +---+-------+ +--------+ +---+---+
go func() ---> |G| ---> |P|local| <===balance===> |global| <--//--- |P|M|
+---+ +---+-------+ +--------+ +---+---+
| | |
| +---+ | |
+----> |M| <---findrunnable---+---steal<--//------+
+---+
| 1. 语句go func() 创建G
| 2. 放入P本地队列(或平衡到全局队列)
+---execute<-----schedule 3. 唤醒或新建M执行任务
| | 4. 进入调度循环schedule
| | 5. 竭力获取待执行G任务并执行
+-->G.fn-->goexit--+ 6. 清理现场,重新进入调度循环
Go Scheduler
思想
-
复用线程
-
work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M’那里偷取G。
-
hand off:当M因为G进行系统调用阻塞时,释放绑定的P,把P转移给其他空闲的M上执行
-
利用并行
-
默认为CPU数量GOMAXPROCS(逻辑),最多有GOMAXPROCS个线程,可能分布在不同的核上运行
-
可以控制并发数量,以达到控制资源利用的目的
策略
-
抢占
- 1个Goroutine最多占用CPU10ms
- 1个Goroutine系统调用超过20μs切换
- 全局G队列
- 当work stealing从其他P中偷不到G时,从全局G队列中取G
G
type g struct {
stack stack // 描述了真实的栈内存,包括上下界
m *m // 当前的m
sched gobuf // goroutine切换时,用于保存g的上下文
param unsafe.Pointer // 用于传递参数,睡眠时其他goroutine可以设置param,唤醒时该goroutine可以获取
atomicstatus uint32
stackLock uint32
goid int64 // goroutine的ID
waitsince int64 // g被阻塞的大体时间
lockedm *m // G被锁定只在这个m上运行
}
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer
ret sys.Uintreg
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
M
type m struct {
g0 *g // 带有调度栈的goroutine
gsignal *g // 处理信号的goroutine
tls [6]uintptr // thread-local storage
mstartfn func()
curg *g // 当前运行的goroutine
caughtsig guintptr
p puintptr // 关联p和执行的go代码
nextp puintptr
id int32
mallocing int32 // 状态
spinning bool // m是否out of work
blocked bool // m是否被阻塞
inwb bool // m是否在执行写屏蔽
printlock int8
incgo bool // m在执行cgo吗
fastrand uint32
ncgocall uint64 // cgo调用的总数
ncgo int32 // 当前cgo调用的数目
park note
alllink *m // 用于链接allm
schedlink muintptr
mcache *mcache // 当前m的内存缓存
lockedg *g // 锁定g在当前m上执行,而不会切换到其他m
createstack [32]uintptr // thread创建的栈
}
P
type p struct {
lock mutex
id int32
status uint32 // 状态,可以为pidle/prunning/...
link puintptr
schedtick uint32 // 每调度一次加1
syscalltick uint32 // 每一次系统调用加1
sysmontick sysmontick
m muintptr // 回链到关联的m
mcache *mcache
racectx uintptr
goidcache uint64 // goroutine的ID的缓存
goidcacheend uint64
// 可运行的goroutine的队列
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr // 下一个运行的g
sudogcache []*sudog
sudogbuf [128]*sudog
palloc persistentAlloc // per-P to avoid mutex
pad [sys.CacheLineSize]byte
}
type schedt struct {
goidgen uint64
lastpoll uint64
lock mutex
midle muintptr // idle状态的m
nmidle int32 // idle状态的m个数
nmidlelocked int32 // lockde状态的m个数
mcount int32 // 创建的m的总数
maxmcount int32 // m允许的最大个数
ngsys uint32 // 系统中goroutine的数目,会自动更新
pidle puintptr // idle的p
npidle uint32
nmspinning uint32
// 全局的可运行的g队列
runqhead guintptr
runqtail guintptr
runqsize int32
// dead的G的全局缓存
gflock mutex
gfreeStack *g
gfreeNoStack *g
ngfree int32
// sudog的缓存中心
sudoglock mutex
sudogcache *sudog
}
Go并行机制
初始化
- osinit()(获取cpu个数和page的大小)
- schedinit()(获取当前G,设置M数量10000,栈空间初始化,内存初始化,当前M初始化,设置P数量为CPU数量,调用procresize(procs)初始化P)
- procresize(procs)(初始化P,新建P对象,将P保存到allp数组,如果P没有cache,进行分配)
- 释放未使用的P(将本地队列中的任务从尾部取出,放入全局队列头部,释放P绑定的cache,将当前P的G复用链接到全局)
G创建过程
- newproc函数创建主G
- 主函数执行runtime.main,创建一个线程负责运行时系统监控
defer
- 每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来.
- 当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行.
- 如果defer执行的是函数,则在defer定义时就把值复制给defer,并保存起来
- 如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.
- 如果defer执行的是闭包,则会在defer函数真正调用时根据整个上下文确定当前的值
defer 调用过程(代码)
return n
编译后
返回值=n
call defer func // 可以操作返回值
return
defer 调用过程(源码)
type _defer struct {
siz int32 // 函数的参数总大小
started bool // defer 是否已开始执行?
sp uintptr // 存储调用 defer 函数的函数的 sp 寄存器值
pc uintptr // 存储 call deferproc 的下一条汇编指令的指令地址
fn *funcval // 描述函数的变长结构体,包括函数地址及参数
_panic *_panic // 正在执行 defer 的 panic 结构体
link *_defer // 链表指针
}
- 先调用runtime.deferproc
- 再调用runtime.deferreturn
- 最后调用函数return
runtime.deferproc
- 将defer后的函数及相关参数生成结构体挂在当前goroutine的_defer链表头部
- 新
defer
指针指向上一个defer(栈) - 检查
deferproc
的返回值,0表示成功,继续处理defer声明或后续代码 - 如果返回值不为0,那么会跳到return之前的deferreturn
runtime.deferreturn
- 执行defer链表中的函数,直到链表为空
unsafe.Pointer
- 任何类型可以转化为unsafe.Pointer
- unsafe.Pointer可以转化为任何类型
- uintptr可以转化为unsafe.Pointer
- unsafe.Pointer可以转化为uintptr
垃圾回收(GC)
- v1.1 STW
- v1.3 Mark STW, Sweep并行
- v1.5 三色标记
- v1.8 hybtid writ barrire
SetFinalizer
obj被回收时,需要进行一些操作,如写日志等,可通过调用函数
runtime.SetFinalizer(obj interface{}, finalizer interfice{})
obj为指针类型
finalizer为参数为obj且无返回值的函数
当GC发现obj不可达时,会在独立goroutine中执行finalizer(obj)
三色标记
- 起初所有对象为白色
- 扫描找出所有可达对象,标记为灰色,放入待处理队列
- 从队列提取灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
- 写屏障监视对象内存修改,重新标色或放入队列
扫描和标记完成后,白色为回收对象黑色为活跃对象
写屏障
在执行写操作之前的代码,设置写屏障
GC循环
- 扫描终止,标记,标记终止,清理
+-----------循环 N----------------+
+ 扫描终止、标记/标记终止 +
+ | | +
+ \ / +
+ 标记终止完成/清理 + ===> +------循环 N+1------+
plan9汇编
程序基本分段
- .DATA: 初始化变量
- .DATA symbol+offset(SB)/width, value
- .DATA msg<>+0x00(SB)/8, $"Hello, W"
- .GLOBL: 声明全局变量
- .GLOBL msg<>(SB),NOPRT,$16
- .BSS: 未初始化的全局变量
- .TEXT: 定义函数
- .TEXT symbol(SB), [flags,] $framesize[-argsize]
- .RODATA: 只读数据段
<>只在当前文件中可以访问
基本指令
SUBQ $0x18, SP // 对SP做减法,为函数分配函数栈帧
ADDQ $0x18, SP // 对SP做加法,清除函数栈帧
$num // 常数
MOVB $1, DI // 1 byte
MOVW $0x10, BX // 2 bytes
MOVD $1, DX // 4 bytes
MOVQ $-10, AX // 8 bytes
// 常见计算指令
ADDQ AX, BX // BX += AX
SUBQ AX, BX // BX -= AX
IMULQ AX, BX // BX *= AX
// 无条件跳转
JMP addr // 跳转到地址,地址可为代码中的地址,不过实际上手写不会出现这种东西
JMP label // 跳转到标签,可以跳转到同一函数内的标签位置
JMP 2(PC) // 以当前指令为基础,向前/后跳转 x 行
JMP -2(PC) // 同上
// 有条件跳转
JNZ target // 如果 zero flag 被 set 过,则跳转
-
FP: 使用形如
symbol+offset(FP)
的方式,引用函数的输入参数。例如arg0+0(FP)
,arg1+8(FP)
,使用 FP 不加 symbol 时,无法通过编译 -
PC: 实际上就是在体系结构的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。
-
SB: 全局静态基指针,一般用来声明函数或全局变量。
-
SP: plan9 的这个 SP 寄存器指向当前栈帧的局部变量的开始位置,使用形如
symbol+offset(SP)
的方式,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是 8 字节,那么第一个局部变量就可以用localvar0-8(SP)
来表示。 -
go语言参数和返回值都在栈上传递
-
c语言前6个参数和返回值在寄存器上传递
源码
package main
func main() {
s := make([]int, 3, 10)
_ = f(s)
}
func f(s []int) int {
return s[1]
}
汇编代码
"".f STEXT nosplit size=53 args=0x20 locals=0x8
// 栈帧大小为8字节,参数和返回值为32字节
0x0000 00000 (main.go:8) TEXT "".f(SB), NOSPLIT, $8-32
// SP栈顶指针下移8字节
0x0000 00000 (main.go:8) SUBQ $8, SP
// 将BP寄存器的值入栈
0x0004 00004 (main.go:8) MOVQ BP, (SP)
// 将新的栈顶地址保存到BP寄存器
0x0008 00008 (main.go:8) LEAQ (SP), BP
0x000c 00012 (main.go:8) FUNCDATA $0, gclocals·4032f753396f2012ad1784f398b170f4(SB)
0x000c 00012 (main.go:8) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
// 取出slice的长度len
0x000c 00012 (main.go:8) MOVQ "".s+24(SP), AX
// 比较索引1是否超过len
0x0011 00017 (main.go:9) CMPQ AX, $1
// 如果超过len,越界了。跳转到46
0x0015 00021 (main.go:9) JLS 46
// 将slice的数据首地址加载到AX寄存器
0x0017 00023 (main.go:9) MOVQ "".s+16(SP), AX
// 将第8byte地址的元素保存到AX寄存器,也就是salaries[1]
0x001c 00028 (main.go:9) MOVQ 8(AX), AX
// 将结果拷贝到返回参数的位置(y)
0x0020 00032 (main.go:9) MOVQ AX, "".~r1+40(SP)
// 恢复BP的值
0x0025 00037 (main.go:9) MOVQ (SP), BP
// SP向上移动8个字节
0x0029 00041 (main.go:9) ADDQ $8, SP
// 返回
0x002d 00045 (main.go:9) RET
0x002e 00046 (main.go:9) PCDATA $0, $1
// 越界,panic
0x002e 00046 (main.go:9) CALL runtime.panicindex(SB)
0x0033 00051 (main.go:9) UNDEF
0x0000 48 83 ec 08 48 89 2c 24 48 8d 2c 24 48 8b 44 24 H...H.,$H.,$H.D$
0x0010 18 48 83 f8 01 76 17 48 8b 44 24 10 48 8b 40 08 .H...v.H.D$.H.@.
0x0020 48 89 44 24 28 48 8b 2c 24 48 83 c4 08 c3 e8 00 H.D$(H.,$H......
0x0030 00 00 00 0f 0b .....
rel 47+4 t=8 runtime.panicindex+0
内存重排
CPU架构
Total store ordering (TSO)
最终结果
false sharing
- 避免缓存行频繁更新
- 使用其他数据填充缓存行
Goroutine调度原理图
初始化
- runtime.init()
- main.init() // 所有导入包(包括标准包)初始化
- main.main()
- 所有init都在一个goroutine中执行
- 所有init结束后才会执行main.main
命令行
- 编译
go bulid -gcflags "-N -l" -o xxx
-N -l:禁用编译器初始化
- 反编译
最终机器码汇编
go tool objdump -s "runtime\.init\b" xxx
过程汇编
go build -gcflags -S xxx.go
go tool compile -N -l -S xxx.go
- trace
package main
import (
"os"
"runtime/trace"
)
func main() {
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
// Your program here
}
查看
go tool trace trace.out
- cpu profile
go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
将分析结果写入cprof
- pprof
go tool pprof --text http.test cprof
--list funcname // 查看
--web // web可视化
- go test [,-op]=[regexp]
go test
默认测试所有*_test.go文件
后跟具体文件名表示测试指定文件
-bench=regexp
指向相应的benchmarks
-cover
覆盖率测试
-run=regexp
只运行regexp匹配的函数
-v
显示详细测试命令
编译指示
指示 | 作用 |
---|---|
//go:noinline |
不要内联。 |
//go:nosplit |
跳过栈溢出检测。 |
//go:noescape |
禁止逃逸,而且它必须指示一个只有声明没有主体的函数 |
//go:norace |
跳过竞态检测 |
encoding/gob
s := test{
data: make(map[string]interface{}, 8),
}
s.data["count"] = 8
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
enc.Encode(s.data)
b := buf.Bytes()
fmt.Println(b)
dec := gob.NewDecoder(bytes.NewBuffer(b))
s2 := test{
data: make(map[string]interface{}, 8),
}
ds := dec.Decode(&s2.data)
fmt.Println(ds)
for _, v := range s2.data {
fmt.Printf("%v\t%[1]T", v)
}
-
使用gob序列化Go对象,反序列化时可以保持原有Go对象类型(JSON无法保持浮点,整型)
-
引用类型或包含引用类型在32bit平台为4Byte,64bit平台为8Byte