Go
Last updated
Dec 9, 2022
# 输入输出
https://blog.csdn.net/weixin_44211968/article/details/124632136
字节 byte 类型也属于整型
证明了基于数组的切片,使用的底层数组还是原来的数组,一旦修改切片的元素值,那么底层数组对应的值也会被修改。
# 1.GMP goroutine machine processor

# 通知协程退出的方式
- 通过全局变量:如果全局变量为真就退出
如果worker中再启动goroutine,就不太好控制了 - 通过通道:协程在通道里面取到true就退出
使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel - 通过context:通过调用ctx.Done()方法通知所有的协程退出
当子goroutine又开启另外一个goroutine时,只需要将ctx传入即可 - context.WithTimeout超时退出
取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制
# init() 函数是什么时候执行的
init()
函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
拍
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init()
函数。同一个包,甚至是同一个源文件可以有多个 init()
函数。init()
函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init()
函数的执行顺序不作保证。
一句话总结: import –> const –> var –> init()
–> main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| package main
import "fmt"
func init() {
fmt.Println("init1:", a)
}
func init() {
fmt.Println("init2:", a)
}
var a = 10
const b = 100
func main() {
fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10
|
# 局部变量分配在栈上还是堆上
由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。
1
2
3
4
5
6
7
8
9
| func foo() *int {
v := 11
return &v
}
func main() {
m := foo()
println(*m) // 11
}
|
foo()
函数中,如果 v 分配在栈上,foo 函数返回时,&v
就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值。
# 调度方式
# 协作式调度

主动调用Gosched/Goexit方法去执行调度逻辑,遇到阻塞或者GC时都会主动去调用这两个方法,把CPU交给其他协程
runtime.Gosched() : 会将之前的G放回待运行队列,之前的G在后面会被调度到。
runtime.gopark() :不会将之前的G放回待运行队列,之前的G需要等待其他G恢复才能执行。
runtime.Goexit() :不会将之前的G放回待运行队列,之前的G会被回收。
# 基于协作的抢占式调度
某些协程执行时间过长,导致其他协程得不到调度,任务执行时延高。
垃圾回收的时候需要STW,需要让所有执行的协程暂停工作,但是协作式调度需要等待G主动让出CPU的时候才能执行到调度器,而且还需要等待所有的G都停止工作,其时间可想而知,极端情况下是十分漫长的。
基于协作的抢占是通过给G设置标志位(stackguard0)实现的。当G在函数调用的时候会检查这个标志位,当其为StackPreempt 时,那就说明当前G被别的G抢占,就主动去执行调度代码。
下面是一张描述多个协程调度过程中,G3协程被监控线程(sysmon)检测到超时运行后基于协作的抢占调度的图:

# 基于信号的抢占式调度
虽然基于协作的抢占式调度解决了一部分问题,但是它还是不够完备。
在一些极端情况下,还是会出现比较严重的问题,比如协程长时间执行并且不会执行到抢占标志检查就不会触发调度
基于信号的抢占式调度是非协作式抢占调度
下面是一张描述多个协程调度过程中,G3协程被监控线程(sysmon)检测到超时运行后发生基于信号抢占调度的图:

基于信号的抢占调度第一步肯定是要注册信号的处理事件,这个过程在上图没有展示,因为注册信号这个动作是M0线程做的,对应信号的处理事件是全局共享的。
信号:SIGURG(即上文提到的sigPreempt)
回调函数:func doSigPreempt(gp g, ctxt sigctxt)
触发抢占,发送信号给要被抢占G的M
这个动作可以看上图的(C -> F):
C: 检查超时运行的协程
D: 发现G3运行时间大于10ms
E,F: 给G3所在线程发送抢占信号
G3所在CPU执行注册的软中断事件
这个动作可以看上图的(9->11):
-9: 开始处理中断
-10: 修改寄存器植入指令
-11: 中断结束,返回G3线程用户态
G3所在CPU执行注册的软中断事件
这个动作可以看上图的(9->11):
-9: 开始处理中断
-10: 修改寄存器植入指令
-11: 中断结束,返回 G3线程用户态
# 2.锁
# 3.interface
# 2个interface可以比较吗
Go 语言中,interface 的内部实现包含了 2 个字段,类型 T
和 值 V
,interface 可以使用 == != 比较
两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
类型 T 相同,且对应的值 V 相等。
type Stu struct {
Name string
}
type StuInt interface{}
func main() {
var stu1, stu2 StuInt = &Stu{“Tom”}, &Stu{“Tom”}
var stu3, stu4 StuInt = Stu{“Tom”}, Stu{“Tom”}
fmt.Println(stu1 == stu2) // false
fmt.Println(stu3 == stu4) // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
| `stu1` 和 `stu2` 对应的类型是 `*Stu`,**值是 Stu 结构体的地址,两个地址不同**,因此结果为 false。
`stu3` 和 `stu4` 对应的类型是 `Stu`,**值是 Stu 结构体,且各字段相等**,因此结果为 true。
## 4.GC
程序中定义一个变量,会在内存中开辟相应内存空间进行存储,当不需要此变量后,需要手动销毁此对象,并释放内存。而这种对不再使用的内存资源进行自动回收的功能即为**垃圾回收(Garbage Collection,缩写为GC),是一种自动内存管理机制**
1. 如何识别垃圾
**引用计数算法(reference counting)**
引用计数通过在对象上增加自己被引用的次数,被其他对象引用时加1,引用自己的对象被回收时减1,引用数为0的对象即为可以被回收的对象,这种算法在内存比较紧张和实时性比较高的系统中使用比较广泛,如php,Python等。
优点:
方式简单,回收速度快
缺点:
需要额外的空间存放计数
无法处理循环引用(如a.b=b; b.a=a)
频繁更新引用计数降低了性能
**追踪式回收算法(Tracing)**
追踪式算法(可达性分析)的核心思想是判断一个对象是否可达,如果这个对象一旦不可达就可以立刻被GC回收了,那么我们怎么判断一个对象是否可达呢?第一步从根节点开始找出所有的全局变量和当前函数栈里的变量,标记为可达。第二部,从已经标记的数据开始,进一步标记它们可访问的变量,以此类推,专业术语叫传递闭包。当追踪结束时,没有被打上标记的对象就被判定是不可触达。
优点:
解决了循环引用的问题
占用的空间少了
缺点:
无法立刻识别出垃圾对象,需要依赖GC线程
算法在标记时必须暂停整个程序,即STW(stop the world),否则其他线程有可能会修改对象的状态从而回收不该回收的对象
2. 如何清理垃圾
**标记清除算法(Mark Sweep)**
标记清除算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:
1 标记阶段:暂停应用程序的执行,从根对象触发查找并标记堆中所有存活的对象;
2 清除阶段:遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存**加入空闲链表**,恢复应用程序的执行;

优点:
实现简单。
缺点:
执行期间需要把整个程序完全暂停,不能异步的进行垃圾回收。
容易产生大量不连续的内存随便,碎片太多可能会导致后续没有足够的连续内存分配给较大的对象,从而提前触发新的一次垃圾收集动作。
**标记复制算法**
它把内存空间划分为两个相等的区域,每次只使用其中一个区域。在垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收对象进行回收。
**标记压缩算法**
在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
3. 设计原理
**三色标记算法**
为了解决原始标记清除算法带来的长时间STW, Go从v1.5版本实现了基于三色标记清除的并发垃圾收集器,**在不暂停程序的情况下即可完成对象的可达性分析**,三色标记算法将程序中的对象分成白色、黑色和灰色三类:
- 白色对象 - 潜在的垃圾,表示还未搜索到的对象,其内存可能会被垃圾收集器回收;
- 黑色对象 - 活跃的对象,表示搜索完成的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象
- 灰色对象 - 活跃的对象,表示正在搜索还未搜索完的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;
三色标记法属于增量式GC算法,回收器首先将所有对象标记成白色,然后从gc root出发,逐步把所有可达的对象变成灰色再到黑色,最终所有的白色对象都是不可达对象。
具体实现:
- 初始时所有对象都是白色的
- 从`gc root`对象出发,扫描所有可达对象标记为灰色,放入待处理队列
- 从队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色,放入队列
- 重复上一步骤,直到灰色对象队列为空
- 此时剩下的所有白色对象都是垃圾对象

优点:
不需要STW
缺点:
如果产生垃圾速度大于回收速度时,可能会导致程序中垃圾对象越来越多而无法及时收集
线程切换和上下文转换的消耗会使得垃圾回收的总体成本上升,从而降低系统吞吐量
三色标记法存在并发性问题,
可能会出现野指针(指向没有合法地址的指针),从而造成严重的程序错误
漏标,错误的回收非垃圾对象
三色不变性
想要在并发或者增量的标记算法中保证正确性,我们需要达成一下两种三色不变性中的任意一种。
- 强三色不变性——黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象。
- 弱三色不变性——黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

**屏障技术**
垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,根据操作类型的不同,我们可以将它们分成读屏障和写屏障两种,因为读屏障需要在读操作中加入代码片段,对用户程序的性能影响很大,所以变成语言往往都会采用写屏障保证三色不变性。
插入写屏障
当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变性,不会存在黑色对象引用白色对象。
删除写屏障
在灰色对象删除对白色对象的引用时,将白色对象置为灰色,其实就是快照保存旧的引用关系,这叫STAB(snapshot-at-the-beginning),以此满足弱三色不变性。
混合写屏障
v1.8版本之前,运行时会使用插入写屏障保证强三色不变性;
在v1.8中,组合插入写屏障和删除写屏障构成了混合写屏障,保证弱三色不变性;该写屏障会将覆盖的对象标记成灰色(删除写屏障)并在当前栈没有扫描时将新对象也标记成灰色(插入写屏障):
写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新建的对象都会被直接标记成黑色。
**执行周期**
Go语言的垃圾收集可以分成清除终止、标记、标记终止和清除四个不同阶段:

**清理终止阶段**
1. 暂停程序,所有的处理器在这时会进入安全点(safe point);
如果当前垃圾收集循环是强制触发的,我们还需要处理还未清理的内存管理单元;
**标记阶段**
1. 将状态切换至`_GCmark`、开启写屏障、用户程序协助(`Mutator Assists`)并将根对象入队;
2. 恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色;
3. 开始扫描根对象,包括所有`Goroutine`的栈、全局对象以及不在堆中的运行时数据结构,扫描`Goroutine`栈期间会暂停当前处理器;
4. 依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色;
5. 使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段;
**标记终止阶段**
1. 暂停程序、将状态切换至`_GCmarktermination` 并关闭辅助标记的用户程序;
2. 清理处理器上的线程缓存;
**清理阶段**
1. 将状态切换至`_GCoff` 开始清理阶段、初始化清理状态并关闭写屏障;
2. 恢复用户程序,所有新创建的对象会标记成白色;
3. 后台并发清理所有的内存管理单元,当`Goroutine`申请新的内存管理单元时就会触发清理;
**GC触发时机**
当足触发垃圾收集的基本条件:允许垃圾收集、程序没有崩溃并且没有处于垃圾循环;
注:运行时会通过如下所示的`runtime.gcTrigger.test`方法决定是否需要触发垃圾收集,该方法会根据三种不同方式触发进行不同的检查。
```go
func (t gcTrigger) test() bool {
if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
return memstats.heap_live >= memstats.gc_trigger
case gcTriggerTime:
if gcpercent < 0 {
return false
}
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
return int32(t.n-work.cycles) > 0
}
return true
}
|
- 超过内存大小阙值,分配内存时,当前已分配内存与上一次
GC
结束时存活对象的内存达到某个比例时就触发GC
。(默认配置会在堆内存达到上一次垃圾收集的2倍时,触发新一轮的垃圾收集,可以通过环境变量GOGC
调整,在默认情况下他的值为100,即增长100%的堆内存才会触发GC
);比如一次回收完毕后,内存的使用量为5M,那么下次回收的机制则是内存分配达到10M的时候,也就是说,并不是内存分配越多,垃圾回收频率越高。 - 如果一直达不到内存大小的阙值,
sysmon
检测出一段时间内(由runtime.forcegcperiod
变量控制,默认为2分钟)没有触发过GC
,就会触发新的GC。 - 调用
runtime.GC()
强制触发GC
GC调优
减少堆内存的分配是最好的优化方式。比如合理重复利用对象;避免string
和byte[]
之间的转化等,两者发生转换的时候,底层数据结构会进行复制,因此导致gc效率会变低,少量使用+
连接string
,Go里面string
是最基础的类型,是一个只读类型,针对他的每一个操作都会创建一个新的string
,如果是少量小文本拼接,用“+”
就好,如果是大量小文本拼接,用strings.Join
;如果是大量大文本拼接,用bytes.Buffer
。
优化努力的方向:
- 尽可能保持最小的堆内存
- 最佳的GC频率
- 保持每次垃圾收集的内存大小
- 最小化每次垃圾收集的STW和Mark Assist的持续时间
# 4.channel
# 底层实现

buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表sendx
和recvx
用于记录buf
这个循环链表中的发送或者接收的indexlock
是个互斥锁。recvq
和sendq
分别是接收(<-channel)或者发送(channel<-xxx)的goroutine抽象出来的结构体(sudog)的队列,是个双向链表。
在堆中创建,实际上就是在堆中实例化了一个如上的结构体,channel本身就是一个指针,所以函数中直接传递,生产或者消费数据时,先加锁,然后sendx\recvx变动。
# 5.grpc
gRPC 和标准库的 RPC 框架有一个区别,gRPC 生成的接口并不支持异步调用。不过我们可以在多个 Goroutine 之间安全地共享 gRPC 底层的 HTTP/2 连接,因此可以通过在另一个 Goroutine 阻塞调用的方式模拟异步调用。
# 6.性能调优
- CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
- Memory Profile(Heap Profile):报告程序的内存使用情况
- Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
- Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的
# 采集性能数据
Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:
runtime/pprof
:采集工具型应用运行数据进行分析net/http/pprof
:采集服务型应用运行时数据进行分析
pprof开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取各个函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。
注意,我们只应该在性能测试的时候才在代码中引入pprof。
# 可视化
