Golang底层初涉(慕课)
第三章
go的特点
一次编码 | 一次编译 | 不需要运行环境 | 没有虚拟化损失 | 不需要自行处理 GC | 面向对象 | 非常易用的并发能力 | |
---|---|---|---|---|---|---|---|
C | X | √ | X | √ | X | X | X |
C++ | X | √ | X | √ | X | √ | X |
Java | √ | X | √ | X | √ | √ | X |
JavaScript | √ | O | √ | X | √ | √ | X |
Go | √ | X | √ | √ | √ | √ | √ |
查看从代码到SSA中间码的整个过程
1 | $env:GOSSAFUNC="main" |
1 | export GOSSAFUNC=main |
1 | go build |
查看 Plan9 汇编代码
1 | go build -gcflags -S main.go |
使用 Modules
1 | go get github.com/Jeffail/tunny |
1 | go get github.com/Jeffail/tunny@0.1.3 #指定版本 |
使用goproxy.cn作为代理
1 | go env -w GOPROXY=https://goproxy.cn,direct |
go.mod 文件追加
1 | replace github.com/Jeffail/tunny => xxx/xxx # 替代为本地的路径,而不是到远程拉取 |
go vender 缓存到本地
执行命令”go mod vendor”会将项目依赖的包下载到本地并保存在项目的vendor目录下。
1 | go mod vendor # 把依赖的包一次性缓存到本地 |
1 | go build -mod vendor # 执行本地缓存的包,不会去远程拉取 |
创建 Go Module
1 | go mod init github.com/imooc/moody |
个人
go的runtime
runtime.a永远会一起链接进可执行程序
g0是第一个启动的协程,g0是为了调度协程而产生的协程。随后创建一个主协程,执行用户的main方法。
GO是面向对象吗?
——官方回答:**”Yes and No”**
- Go允许OO的编程风格
- Go的Struct可以看作其他语言的Class
- Go缺乏其他语言的继承结构(平时用的“继承方法”那是另一回事)
- Go的接口与其他语言有很大差异
Go的继承
- Go并没有继承关系
- 所谓Go的继承只是组合
- 组合中的匿名字段,通过语法糖达成了类以继承的效果
总结
◆Go没有对象、没有类、没有继承
◆Go通过组合匿名字段来达到类似继承的效果
◆通过以上手段去掉了面向对象中复杂而冗余的部分
◆保留了基本的面向对象特性
第四章
基本类型大小
1 | package main |
空结构体在内存中有地址而长度为0,所有的空结构体对象的地址都是一样的。(除非被嵌套在其它结构体中)
runtime-malloc.go:
字符串的底层是一个结构体,任何字符串实例的sizeof都是一样的(16字节):
字符串转StringHeader
1 | func main() { |
字符串的两种遍历
1 | str := "快乐 everyday" |
字符串的访问
◆对字符串使用len方法得到的是字节数不是字符数
◆对字符串直接使用下标访问,得到的是字节
◆字符串被range遍历时,被解码成rune类型的字符
◆UTF-8编码解码算法位于runtime/utf8.go
使用utf-8变长编码的字符串时,怎么切分含中英文的字符串呢?以取前三个字符为例:
切片的三种创建方式
1 | arr[0:3] or slice[0:3] |
切片的sizeof总是24(比字符串多一个cap)
比如a=[1,2],然后append(a,1,2,3,4),此时期望容量大于原容量的两倍。
map的底层结构:
unsafe.Pointer为万能指针,指向任意对象。
非溢出桶的数量为2^B。桶内的元素,其key的哈希值相同。通过种子(hash0)的到一个值,对2^B取余得到桶号。tophash的计算如下(取最前的8位):
tophash、keys和elems是三个数组。overflow指向溢出桶。
如果tophash碰撞得比较厉害,就继续顺序往下找(包括到溢出桶去找)。
总结:
◆Go语言使用拉链实现了hashmap
◆每一个桶中存储键哈希的前8位
◆桶超出8个数据,就会存储到溢出桶中
map的扩容
map溢出桶太多时会导致严重的性能下降
runtime.mapassign(0可能会触发扩容的情况:
- 装载因子超过6.5(平均每个槽6.5个ky)
- 使用了太多溢出桶(溢出桶成了一条链表,超过了普通桶)
步骤1
创建一组新桶
oldbuckets指向原有的桶数组
buckets指向新的桶数组
map标记为扩容状态
步骤2
将所有的数据从旧桶驱逐到新桶
采用渐进式驱逐
每次操作一个旧桶时,将旧桶数据驱逐到新桶
读取时不进行驱逐,只判断读取新桶还是引旧桶
B的值加1,某个旧桶的数据会被驱逐到最多两个新桶中(根据新桶号)
步骤3
所有的旧桶驱逐完成后,oldbuckets回收
map采用的是增量扩容,它通过每一次的 map 操作行为去分摊总的一次性动作。(详细自行了解)
map的并发问题
提前总结
◆map在扩容时会有并发问题
◆sync.Map使用了两个map,分离了扩容问题
◆不会引发扩容的操作(查、改)使用read map
◆可能引发扩容的操作(新增)使用dirty map
如果在程序运行中,发生对一个map出现并发冲突,那么会直接爆异常。也就是非并发安全的map选择了干脆禁止同时读写(读写分离)。map的并发问题主要集中在扩容时的影响。
map并发问题解决方案
- 给map加锁(mutex)
- 使用sync.Map
给map加锁的话,会严重影响程序的并发性,go为我们提供了sync.Map。(适用于追加少。如果经常追加那么其实和map加锁差不多)
万能指针是entry的唯一成员。amended
表示“追加”,是一个布尔值。上图演示了把a:A
改成a:AAA
的过程。
此过程如下:
- read-map中找不到key为
d
amended
置为true
,并给dirty加锁(图中mu
的作用)- 在dirty追加数据,完毕后释放锁
此过程如下:
- read-map中找不到key为
d
- 发现
amended
为true
,去dirty查找 - 在dirty找到了数据,然后给
misses
(未命中)加1 - 如果
misses
达到了len(dirty)
,则作dirty提升。
misses
置0,amended
置false
。当再次需要dirty时,才会重建dirty。
正常通过read-map进行删除时没问题的。问题出在追加后删除,关键在于提升dirty时的问题。
Go隐式接口特点
1 | package main |
iface记录了“类型”与“数据”。只有当这两个属性都为nil时,接口才为nil。
空接口的底层跟普通接口不一样,空接口的底层是eface结构体。
当使用结构体作为方法的receiver时,会自动再生成一个以对应结构体的指针作为recevier的方法(如下图),而反之则没有,在实现接口时要注意这一点。
空指针
- nil是空,并不一定是”空指针”
- nil是6种类型的”零值”
- 每种类型的nil是不同的,无法比较
可见不包括结构体,这表明任何结构体实例都不等于nil(即使是空结构体)。
以上定义在builtin包里,这个包表示“内置”,定义了许多我们熟知的类型:
结构体和指针实现接口
1 | type Car interface { |
变量对齐
1 | type Args struct { |
1 |
|
1 | func main() { |
Go中每一个变量都有自己的对齐系数
对齐系数的含义是:变量的内存地址必须被对齐系数整除
如果对齐系数为4,表示变量内存地址必须是4的倍数
字长(Word length)是指计算机处理数据的基本单元的位数,它表示计算机能够一次处理的二进制位数或字节数。以下以字长为64位(即八字节)为例。
假如非字长对齐,那么内存的原子性与效率受到影响。
对齐系数:(一般等于自身大小)
string的大小为16,但对齐系数为8。每个成员的偏移量是自身大小与其对齐系数较小值的倍数。
结构体既需要内部对齐,又需要外部填充对齐。
注意结构体中成员的顺序在内存中是严格的。
结构体的对齐系数是其成员的最大对齐系数。
空结构体
- 空结构体单独出现时,地址为zerobase
- 空结构体出现在结构体中时,地址跟随前一个变量
- 空结构体出现在结构体末尾时,需要补齐字长
1 | type Demo struct { |
1 | type Demo struct { |
这是为了防止内存分配的下一个变量的地址与z撞车。
字长对齐
- 对于特定系统,也有系统对齐系数,一般为系统字长
- 变量要尽量放置在一个系统字长里
1 | var a bool |
第五章
5-2节代码
1 | func do3() { |
协程本质
关于协程,我觉得这篇协程的原理以及与线程的区别 - rhyme - 博客园 (cnblogs.com)讲挺好的。
协程
◆协程就是将一段程序的运行状态打包,可以在线程之间调度
◆将生产流程打包,使得流程不固定在生产线上
◆协程并不取代线程,协程也要在线程上运行
◆线程是协程的资源,协程使用线程这个资源
协程的优势
◆资源利用
◆快速调度
◆超高并发
协程的底层结构(因为协程本身就是go程序去管)
- runtime中,协程的本质是一个g结构体
- stack:堆栈地址
- gobuf:目前程序运行现场
- atomicstatus:协程状态
下图展示了g结构体的一些关键部分:
每个goroutine有自己的栈空间。
通过sp得知执行到哪个函数了,通过pc得知执行到哪一行了。
m结构体:(线程归内核管,只记录了线程的信息)(比如当前执行的协程为哪一个)
协程工作
过程如下:
- 线程一开始进入的是g0的协程栈,执行其schedule方法
- schedule找到一个可执行的协程g,传给execute函数
- execute函数做了一些初始化工作,随后进入gogo函数
- gogo函数是汇编实现的,它向g中人为地在函数栈中插入一段
goexit函数
,这是为了最后返回到此函数中 - gogo函数随后根据sp和pc,从协程的某个位置继续执行
- 返回到goexit函数后,该函数将线程的调用栈切换回g0
注意上述这个过程始终是运行在线程上的。为了适应多核,后来又加入了多线程循环。
==runnable queue
==(可用协程队列)需要加锁。
总结-线程循环
◆操作系统并不知道Goroutine的存在
◆操作系统线程执行一个调度循环,顺序执行Goroutine
◆调度循环非常像线程池
问题
◆协程顺序执行,无法并发
◆多线程并发时,会抢夺协程队列的全局锁
GMP调度模型
GMP调度模型旨在解决上述第二个问题。
GMP对应了g, m, p三个结构体。p是processer的简称,p结构体内含有指向其服务的m的指针,以及一个储存g结构体指针的本地队列。m可以无锁从本地队列中取出可用协程。runnext指向队列中的下一个可用协程。
当P中的协程用完时,会到全局队列中批量获取协程,该过程仍需要获取锁。
任务窃取:如果M在本地或者全局队列中都找不到可用G,去别的P中“偷”,这样增强了线程的利用率。
新建协程
◆随机寻找一个P
◆将新协程放入P的runnext(插队)(优先执行新建协程)
◆若P本地队列满,放入全局队列
协程并发
◆如果协程顺序执行,会有饥饿问题
◆协程执行中间,将协程挂起,执行其他协程
◆完成系统调用时挂起,也可以主动挂起
◆防止全局队列饥饿,本地队列随机抽取全局队列
这里放回的是本地队列,但这样可能会引起全局队列中的协程饥饿。
go底层源码的解决方法是:每从P结构体的队列拿61个协程以后,就到全局队列去拿一个。
切换时机:
系统调用结束时会挂起。
主动挂起不是说自己调用gopark函数(注意小写字母开头)。像Sleep这样的函数底层就调用了gopark。
抢占式调度
基于协作的抢占式调度
问题——如果程序:
◆永远都不主动挂起
◆永远都不系统调用
那么便不能及时切换。考虑到有什么过程会在程序中被经常调用,go为morestack函数
赋予了其他功能
调用函数时,需要先通过morestack函数来检查协程栈是否充足。同时,morestack
会检查协程是否处于被标记抢占(系统监控器负责)的状态,如果是,回到schedule()。
问题:以下代码无法切换协程。
1 | func s() { |
go将SIGURG(紧急信号)用作调度用途。
无限开启协程
1 | func main() { |
利用 channel 的缓存区
1 | func main() { |
此代码会因为并发操作数量过多而在运行中爆panic。
协程池
1 | go env -w GOPROXY=https://goproxy.cn,direct |
1 | func main() { |
老师并不推荐用协程池(因为go的协程调度已经够池化了。◆Go语言的初衷是希望协程即用即毁,不要池化)
协程太多的问题
◆文件打开数限制
◆内存限制
◆调度开销过大
总结
◆太多的协程会给程序运行带来性能和稳定性问题
◆牺牲并发特性,利用channe|缓冲
第六章
Atomic 机制
- CPU 级别支持的原子操作
- X86平台:给内存加锁,再操作
- Arm平台:先操作,如果操作失败,再重试
1 | func add(p *int32) { |
atomic操作运用了硬件层面锁
CAS
1 | ok = atomic.CompareAndSwapInt32(&value, value, newValue) |
在Go语言中,CAS(Compare and Swap)是一种原子操作,用于实现并发安全的内存访问。CAS操作由
sync/atomic
包提供,它允许你比较一个内存地址的值与旧值,并在相等时将新值写入该地址。CAS操作可以用于实现无锁的并发算法,如自旋锁、无锁队列(应用在头和尾指针的Swap中)等。CAS操作在并发环境中通常包含以下步骤:
- 读取内存地址的当前值。
- 比较当前值与期望的旧值。如果它们相等,说明内存地址的值没有被其他线程修改,可以进行更新操作。
- 如果当前值与旧值相等,则将新值写入内存地址。
- 如果当前值与旧值不相等,则说明其他线程已经修改了内存地址的值,CAS操作失败,需要重新尝试。
CAS操作是原子的,意味着它在执行期间不会被其他线程中断。如果多个线程同时尝试执行CAS操作,只有一个线程能够成功,其他线程需要重新尝试或执行其他操作。
sema锁
底层仍有atomic操作,Uint32是接下去的重点。
treap指针指着一颗平衡二叉树。二叉树的每个节点是一个sudog结构体,每个结构体内对应有一个协程(g结构体)。
sema操作(uint32>0)
获取锁:uint32减一,获取成功
释放锁:uint32加一,释放成功
查看semacquire1
函数。sema锁的uint32值为0时,协程会加入二叉树并阻塞在以下这一步(s是当前协程的sudog)——
总结
原子操作是一种硬件层面加锁的机制
数据类型和操作类型有限制
sema锁是runtimel的常用工具
sema经常被用作休眠队列
互斥锁
底层结构:
WaiterShift表示阻塞在这把锁上的协程数量,占29位。
当锁被抢占时(即Locked为1),当前协程会尝试多次自旋(Spin),重复判断Locked,随后才去获取sema。
由于sema被配置为0,所有获取它的行为必然失败并导致进入二叉树休眠,相当于是一个等待队列。
WaiterShift加一。当锁被释放时,再从平衡二叉树唤醒一个休眠的协程。
被唤醒协程仍要和新来的协程继续竞争锁,这可能导致某些协程迟迟获取不到锁,造成饥饿现象。
总结
mutex正常模式:自旋加锁+sema休眠等待
mutex正常模式下,可能有锁饥饿的问题
锁饥饿
Starving位标志饥饿模式。
总结
锁竞争严重时,互斥锁进入饥饿模式
饥饿模式没有自旋等待,有利于公平
优化经验
减少锁的使用时间(减少粒度)
善用defer确保锁的释放
读写锁的使用
1 | type Person struct { |
waitgroup的使用
1 | type Person struct { |
noCopy标志着此类型的实例不可复制,运行时会检查此值,在违规时报异常。
waiter代表等待的协程数,counter代表正在运行的占有WaitGroup的协程数。
调用Wait方法后,会检查counter是否为0,然后产生不同的行为(见上)。
总结
WaitGroup实现了一组协程等待另一组协程
等待的协程陷入sema并记录个数
被等待的协程计数归零时,唤醒所有sema中的协程
锁拷贝问题
- 锁拷贝可能导致锁的死锁问题
- 使用 vet 工具可以检测锁拷贝问题
- vet 还能检测可能的 bug 或者可疑的构造
1 | func main() { |
1 | go vet |
RACE 竞争检测
1 | var J int |
1 | go build -race main.go |
RACE竞争检测
- 发现隐含的数据竞争问题
- 可能是加锁的建议
- 可能是bug的提醒
dead lock 检测
1 | package main |
go-deadlock检测(这是一个github开源项目)
- 检测可能的死锁
- 实际是检测获取锁的等待时间
- 用来排查bug和性能问题
第七章
Channel 声明方法
- chInt := make(chan int) // unbuffered channel 非缓冲通道
- chBool := make(chan bool, 0) // unbuffered channel 非缓冲通道
- chStr := make(chan string, 2) // bufferd channel 缓冲通道
Channel 基本用法
- ch <- x // channel 接收数据 x
- x <- ch // channel 发送数据并赋值给 x
- <- ch // channel 发送数据,忽略接受者
错误用法
1 | func main() { |
1 | func main() { |
内存与通信
- “不要通过共享内存的方式进行通信”
- “而是应该通过通信的方式共享内存”
1 | func watch(p *int) { |
1 | func watch(c chan int) { |
为什么使用channel来通信
相对于无锁
- 避免协程竞争和数据冲突的问题
相对于加锁
- 更高级的抽象,降低开发难度,增加程序可读性
- 模块之间更容易解耦,增强扩展性和可维护性
channel的底层结构:
前六行描述了一个缓存区(buf指针可能为空,表示为无缓存),后面的成员描述了两个队列。
waitq包含了sudog结构体,表示被阻塞的协程。sudog中含有发送或接受的对象的地址。
可以看到有一把互斥锁lock
互斥锁并不是排队发送/接收数据
互斥锁保护的hchans结构体本身
Channel并不是无锁的
发送和接受
c<-关键字是一个语法糖
编译阶段,会把c<-转化为runtime.chansend1()
chansend1()会调用chansend0方法
发送情况
若接收队列为空,才把变量拷贝进缓存中的可用缓存单元。
总结
接受情况
<-C关键字是一个语法糖
编译阶段,i<-c转化为runtime.chanrecv1()
编译阶段,i,ok<-c转化为runtime.chanrecv2()
总结
编译阶段,<-c会转化为chanrecv()
有等待的G,且无缓存时,从G接收
有等待的G,且有缓存时,从缓存接收
无等待的G,且缓存有数据,从缓存接收
无等待的G,且缓存无数据,等待喂数据
第八章
goroutine-per-connection 编程风格
1 | //TcpServer.go |
IO模型
Socket
很多语言都停供了TCP函数,但是要开发者自己去留意三次握手和四次挥手太过麻烦了,所以很多系统都提供Socket作为TCP网络连接的抽象(平时用到的网络编程实际上基本都是操作Socket)。
IO模型
IO模型指的是同时操作Socket的方案——
◆阻塞
◆非阻塞
◆多路复用
- 同步读写Socket时,线程陷入内核态(每个线程都会因为没有客户端数据而阻塞,阻塞就是内核态)
- 当读写成功后,切换回用户态,继续执行
- 优点:开发难度小,代码简单
- 缺点:内核态切换开销大
- 如果暂时无法收发数据,会返回错误
- 应用会不断轮询,直到Socket可以读写
- 优点:不会陷入内核态,自由度高
- 缺点:需要自旋轮询
epoll全称event poll(有时叫事件池,poll不是池子的意思),我们监听的任务交给操作系统。我们会非阻塞地去epoll获取可读事件(的列表),从而操作对应的socket。
- 注册多个Socket事件
- 调用epool,当有事件发生,返回
- 优点:提供了事件列表,不需要查询各个Scoket
- 缺点:开发难度大,逻辑复杂
Go网络编程
有没有能结合阻塞模型和多路复用的方法?
阻塞模型+多路复用
在底层使用操作系统的多路复用IO
在协程层次使用阻塞模型
阻塞协程时,休眠协程
(协程的休眠不必进入内核态)
首先为了适应不同系统,需要对epoll进行抽象。
epoll、IOPC、Kqueue分别是Linux、window、Mac对多路复用的系统工具
netpoll是go的工具,epoll是其在linux的底层实现。
golang对于epoll的抽象,因为我对epoll还没了解,因此还是得后续再去看一些文章。比如 epoll在Golang的应用 - 知乎 (zhihu.com)
调conn.Read
被阻塞是被卡在底层的WaitRead()
上。
“goroutine-per-connection编程风格”:
- 用主协程监听Listener
- 每个Conn使用一个新协程处理
- 结合了多路复用的性能和阻塞模型的简洁
第九章
9-1 节代码
1 | package main |
Go的协程栈是从Go的堆内存上申请的,栈的释放也是通过GC释放的。(C语言中,栈空间和堆空间严格分开,而Go是包含关系)
栈的结构(重点)
每个栈帧的第一个元素是上一个栈帧的基址,这样以便栈指针回退。
注意顺序,先进入sum的栈帧再进入print。在调用sum前会拷贝实参给形参(可见go是值传递的),同时预留一块内存以接收返回值。通过“返回后的指令”可以继续调研print
协程栈不够大怎么办?
◆局部变量太大(逃逸分析)
◆栈帧太多(栈扩容)
逃逸分析
- 函数返回了对象的指针
1 | package main |
指针逃逸:函数返回了对象的指针
空接口逃逸:
如果函数参数为interface{}
函数的实参很可能会逃逸
因为interface{}类型的函数往往会使用反射(就是在被调用的函数里可能会去分析实参的结构,而这种分析对堆对象更容易)(尤其是使用fmt包时,打印对象往往要通过反射分析其字段。可以改用log包来打印信息)
大变量逃逸:
过大的变量会导致栈空间不足
64位机器中,一般超过64KB的变量会逃逸
Go的堆结构
heapArena
Go每次申请的虚拟内存单元为64MB
最多有4,194,304个虚拟内存单元(2^20)
内存单元t也叫heapArena
所有的heapArena组成了mheap(Go堆内存)
(2^20乘以64MB恰好就是256TB(64位机的虚拟内存空间),这是特别设计的,当然了申请太多会被操作系统给kill掉)
描述堆内存的结构体——
线性分配或链表分配的问题:
分级分配思想:
这个“级”,我们成为span。
内存管理单元mspan
根据隔离适应策略,使用内存时的最小单位为mspan
每个mspan为N个相同大小的“格子”
Go中一共有67种mspan(后续提到68是因为多了一种0级)
在前文的mheap
结构体中,可以看见mspan
数组。
每种mspan并不是一开始就分好的,而是需要时再分配。**mspan
不是按级别顺序挨在一起的**,可能是分散在不同的heapArena中。
那么,在分配内存时,难道需要遍历每个heapArena去寻找合适的mspan
吗?
中心索引mcentral
136个mcentral结构体,其中
68个组需要GC扫描的mspan
68个组不需要GC扫描的mspan(比如常量)
专门开辟一片内存空间作为中心索引。mcentral
结构体实际上是一个链表头。
这样就可以到中心索引中去找最适合的mcentral
,进一步找到适合的mspan
了。
但是,如果一个mcentral
正被一个线程操作,那么就得加锁,这样就不符合高并发场景。因此参考GMP进一步做设计——
线程缓存ncache
每个P(就是GMP的p结构体)拥有一个mcache
一个mcache拥有136个mspan,其中
68个需要GC扫描的mspan
68个不需要GC扫描的mspan
线程直接用所对应的p结构体的mcache就好了。当mcache中某个级别的span满了以后,就会去中心索引中再换取一个。
总结
Go的堆内存分配
Go将对象按照大小分为3种
微小对象使用普通mcache
mcache中,每个级别的mspan只有一个。当mpan满了之后,会从mcentral中换一个新的
mcentral中,只有有限数量的mspan,当mspan缺少时,会从neapArena开辟新的mspan
当neapArena空间不足时,向操作系统申请新的heapArena(扩容)
大对象:直接从neapArena开辟0级的mspan,0级的mspan为大对象定制,可大可小。
GC垃圾回收
三种回收方式
- 标记-清除(最简单直接,Go所使用)(标记为已删除后,下次gc时回收)
- 标记-整理
- 标记-复制
GC思路:找到有引用的对象,剩下的就是没有引用的
结构体中可能还有指向其他对象的指针,因此要做可达性分析:
(上图有误,应该是BFS)对到达不了的变量做标记,此过程中必须暂停其他业务,也就是串行GC
三色标记法
- 黑色:有用,已经分析扫描
- 灰色:有用,还未分析扫描
- 白色:暂时无用
基本过程——
解决并发问题的策略——混合标记(删除屏障+插入屏障)
- 被删除的堆对象标记为灰色(快照标记)
- 被添加的堆对象标记为灰色
如下图,B->C被删除,然后添加E->C,因为有删除屏障,所以C不会被误删除。
如下图,只是添加E->C,但是E已是黑色,导致C被误清除。因此引入了插入屏障。
GC优化
GC触发时机
系统定时触发
用户显式触发
申请内存时触发
(1)系统定时触发
sysmon定时检查
如果2分钟内没有过GC,强制触发
谨慎调整
(2)用户显式触发
用户调用runtime.GC方法
并不推荐调用
(3)申请内存时触发
可见干预触发是不现实的。我们要做的就是尽量少在堆上产生垃圾。
- 内存池化
- 减少逃逸
- 使用空结构体
GC分析工具
1 | go tool pprof |
1 | go tool trace |
1 | go build -gcflags=”-m” |
1 | GODEBUG=”gctrace=1” |
执行$env:GODEBUG="gctrace=1"
,之后运行程序。一条打印信息如下:
@0.011s
表示此次gc发生的时刻(自程序启动后),4%
表示gc占用时间在程序运行中的占比(虽说gc可以并发,但有些任务是不能并发的)。4->6->5MB
表示gc前、中、后的堆内存。
gc占比超过10%
时,最好去做一下优化。
第十章
cgo
1 | package main |
10-2节代码
1 | package main |
1 | package main |
panic基本使用
1 | package main |
panic + defer
1 | package main |
- panic在退出协程之前会执行所有已注册的defer
panic + defer + recover
1 | package main |
对象到反射对象
1 | func main() { |
反射对象到对象
1 | func main() { |
发射调用方法
1 | package main |