1.go的profile工具?
profile就是定时采样,收集cpu,内存等信息,进而给出性能优化指导。golang目前提供了3中profile,分别是 cpu profile, memery profile, blocking profile。
2.能否同时建立大量协程?可以建立,但是后果严重。
考虑到后果:回答不能 ,会造成内存挤压。即go进程分配资源给协程,当这些协程回收之后,go进程仍然占有着协程的资源。系统会为go进程预留空间以免它会再次分配协程。这块实际未使用的内存不会回收。
3.拷贝大切片一定比拷贝小切片代价大吗?
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
LenCap
小于1024 变为原来的两倍 大于1024 变为原来的1.25倍
4.切片的深浅拷贝
Go
=[:]Gocopy()
5.设计模式
6.go调试工具
delve
pprof有两个包:
net/http/pprof 和 runtime/pprof
其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来
7.golang性能分析
time-toolexec
**方法2:**GODUBUG
**方法3:**pprof
pprof
方法4:/debug/pprof
方法5: pref
**方法6:**火焰图 Flame Graph
pprof trans
8.golang map底层原理 什么时候进行扩容? 什么时候进行rehash?
有两种扩容方式:
1.增量扩容
触发 map 扩容的时机:在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容:
2^B
2.overflow 的 bucket (溢出桶)数量过多,这有两种情况:(1)当 B 大于15时,也就是 bucket 总数大于 2^15 时,如果overflow的bucket数量大于215,就触发扩容。(2)当B小于15时,如果overflow的bucket数量大于2B 也会触发扩容。
如何扩容:由于 map 扩容需要将原有的 key/value 重新搬迁到新的内存地址,如果有大量的 key/value 需要搬迁,在搬迁过程中map会阻塞,非常影响性能。因此 Go map 的扩容采取了一种称为 “渐进式” 的方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个bucket。
首先分配新的buckets,并且将老的buckets挂到新的map的oldbucket字段上。在插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。
2.等量扩容 也要创建新的桶 不是原地扩容
此时桶内元素分布非常稀疏,重新做一遍类似增量扩容的搬迁动作,将稀疏的键值对重新排列一次,让桶利用率更高,保证更快地存取。
如果在搬迁的时候进行查询,首先查询新的map里面有没有,如果没有就去oldbucket中查找。如果没有就是没有。
9.malloc分配的内存是物理内存还是虚拟内存?
分配虚拟内存,当要访问该数据的时候才分配物理内存。
10.go有哪些优点?
出生名门,来自谷歌公司。
语法简单。
相较于 Java 和 C++呆滞的编译速度,Go 的快速编译时间是一个主要的效率优势。Go拥有接近C的运行效率。
Go语言支持当前所有的编程范式,包括过程式编程、面向对象编程、面向接口编程、函数式编程。
强大的标准库。
部署方便:二进制文件,Copy部署
创新的地方:有Goroutine 和 Channel,并发简单。并且支持高并发。
很稳定:有很强大的编译检查、严格的编码规范和完整的软件生命周期工具。go tool、gofmt、go test等。
11.牛客网上的代码上传到服务器,服务器如何判定?
首先服务器接收到go文件之后,与服务器本地测试文件结合,在本地测试文件里面调用一个test函数,调用了go文件里的函数,然后执行go run,得出来的结果用 assert + 布尔表达式判定。
12.数组和切片的区别?
先说数据结构
切片是指针类型,数组是值类型。
数组的长度是固定的,而切片不是(切片可以看成动态的数组)。
切片比数组多一个容量(cap)属性。
切片的底层是数组。
13.协程同步的方式,waitgroup和contex的区别?
在go中有三种方式实现并发控制(主协程等待子协程完成):
- Channel:通过channel控制子协程;
- WaitGroup:使用信号量控制子协程;子协程数动态可调整;
- Context:使用上下文控制子协程;可进一步控制孙子协程;
Waitgroup结构:
包含状态计数器 和 一个信号量:
- counter:还未执行完成的协程数量;
- waiter count:多少个等待者;
- semaphore:信号量;
当信号量>0时,表示资源可用;获取信号量时信号量减一;
当信号量==0时,表示资源不可用;获取信号量时,当前线程进入睡眠,当信号量为正时被唤醒。
对外提供接口:
Add(delta int): counter增加delta(delta可以为负,若counter=0,根据waiter数量释放等量的信号量;若counter为负数,则panic。;
Wait(): waiter递增一,并阻塞等待信号量.
Done():counter减一,并按照waiter数释放相应次数信号量,实际上是调用Add(-1);
Context:相比WaitGroup,Context对于派生协程有更强的控制力,可控制多级协程。主要看下面的链接。
14.如何处理异常?
在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
这样就需要一个错误处理机制。
Go 中引入的处理方式为:defer, panic, recover
使用defer+recover来处理异常。
自定义错误
Go 程序中,也支持自定义错误, 使用errors.New 和panic内置函数。
errors.New(“错误说明”) , 会返回一个 error 类型的值,表示一个错误
panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.
举个例子,在要调用的函数后增加一个返回值error,若err不为空,调用 panic让程序终止。
15.defer 的执行顺序,执行时参数的值,以及参数的执行顺序。
defer 是后进先出。 协程遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中,遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。从执行顺序上来看,实际上是按照先进后出的顺序执行defer、
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
执行结果:
打印后
打印中
打印前
panic: 触发异常
例子:
package main
import (
"fmt"
)
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
执行结果: defer中的内置函数先执行,按照正常顺序执行完之后,再执行defer,后进先出。
需要注意到defer执行顺序和值传递 index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用calc(“10”,1,2)>10,1,2,3 执行index:2时,与之前一样,需要先调用calc(“20”,0,2)>20,0,2,2 执行到b=1时候开始调用,index:2==>calc(“2”,0,2)>2,0,2,2 最后执行index:1>calc(“1”,1,3)==>1,1,3,4
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
16.go语言中代码执行流程
引入包->初始化包变量—>执行包里面的init()->main.go中初始化变量->init()->main();
init()函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。init 函数通常被用来:
- 对变量进行初始化
- 检查/修复程序的状态
- 注册
- 运行一次计算
17.交替打印输出1-100
使用channel / var wg sync.WaitGroup / wg.Add(2) / defer wg.Done() /wg.Wait()
package main
import (
"fmt"
"sync"
)
func main() {
//使用channel保证有序
ch := make(chan struct{})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 1; i < 101; i++ {
ch <- struct{}{}
//奇数
if i%2 == 1 {
fmt.Println("线程1打印:", i)
}
}
}()
go func() {
defer wg.Done()
for i := 1; i < 101; i++ {
<-ch
//偶数
if i%2 == 0 {
fmt.Println("线程2打印:", i)
}
}
}()
wg.Wait()
}
18.go读写锁实现原理?
分为读优先和写优先。go里面是写优先。
读写锁 写写之间是互斥的,读写也是互斥的,读读不互斥。
go里面的读写锁由sync包中的RWmutex实现。
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
w : 互斥锁,写协程获得该锁后,其他协程处于等待
writerSem :writer 等待 读完成排队的信号量
readerSem : read 等待 write 完成排队的信号量
readerCount : 读锁的计数器
readerWait : 等待读锁释放的数量
go支持的最高加读锁的数量 1<<30 (2的30次方)
提供了4个接口
- RLock():读锁定
- RUnlock():解除读锁定
- Lock(): 写锁定,与Mutex完全一致
- Unlock():解除写锁定,与Mutex完全一致
读锁
首先有一个全局的计数器叫readercount,它小于0说明目前有写锁在锁定,大于0说明有读锁在锁定。
读加锁时用他是否小于零来判断 是否有写锁定。
reader 是整型数。
在读锁操作的情况下,加一个读锁会为其加一,释放后会减一。
在写锁操作时,会将 rederCount 减 2^30,此时就变成负值。在写锁释放的时候会加 2^30。
所以,在读加锁的时候可以判断这个数是否小于零来确认是否加了 写锁,如果小于零就阻塞等待,保证了 写操作时不会有读操作。
读加锁:**如果想进行读操作,先判断readerCount** 是否小于0.如果小于零,说明目前有写锁进行锁定,进入 runtime_SemacquireMutex 等待写锁释放。反之若 readerCount 大于零,则加锁成功
读解锁:
- 解锁时先 将 readerCount 减一
- 判断 readerCount 的值
- 如果 rederCount 大于零,说明还有 读锁在锁定,本地解锁完成
- 如果readerCount 小于等于0,说明此时已无读锁锁定,如果存在 等待的写锁时,唤醒写锁
写锁
写加锁
readerWait
读操作可以重复加,必然会导致写锁饿死的情况。readerWait可以解决写锁饿死的情况。
当有写锁申请的时候,若 检测到此时有读锁在锁定状态,就会将 readerCount 的值 拷贝到 readerWait 中
每次释放一个读锁时,readerWait 中得值也会减一。
当 readerWait 中得值变为0 时,就会唤醒写锁。
其他的读锁 会一直阻塞,等待写锁释放。
写锁优先,当有写锁来的时候,你们这些读的赶紧读,读完我要写。其他后面来的读的,你们先等着,等我写完后你们再读。
写解锁
在解写锁时,将负值的rw.readerCount变更为正值,解除对读锁的互斥,并唤醒r个因为写锁而阻塞的读操作goroutine。最后,通过调用互斥锁的Unlock办法,解除对写锁的互斥。
19.go可重入锁实现?
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
20.go原子操作有哪些?
sync/atomic提供的原子操作
加法(add)、比较并交换(compare and swap)、加载(load)、存储(store)、交换(swap)。
a.针对int32、int64、uint32、uint64、uintptr类型,提供了上述一套函数;
b.针对unsafe包中的Pointer,提供了除了加法之外的其余四个函数。
此外,sync/atomic包还提供了一个名为Value的类型,可以被用来存储任意类型的值。
21.go原子操作和锁的区别?Go语言里atomic包里的原子操作和sync包提供的同步锁有什么不同呢?
互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。需要形成临界区和创建互斥量的情况下完成并发安全的值替换操作。
**原子操作是针对某个值的单个互斥操作。**atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。
22.go零拷贝?
目的是减少数据的复制次数,
一般拷贝过程:内核态用户态, 磁盘到内核 -内核到用户
零拷贝直接从磁盘到用户 比如 网卡 - socket内核缓冲区 - 用户内核进程
23.go堆栈分别存了什么?
堆:一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不确定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多
栈:由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上
栈、堆逃逸分析:go build -gcflags ‘-m -l’ main.go
24.如何控制goroutine的数量?
总的来说有三种,a.通过有buffer的channel b.通过channel和信号量的组合方式 c.无缓冲的channel和任务做发送和执行分离的方式。
1.通过channel+sync
var (
// channel长度
poolCount = 5
// 复用的goroutine数量
goroutineCount = 10
)
func pool() {
jobsChan := make(chan int, poolCount)
// workers
var wg sync.WaitGroup
for i := 0; i < goroutineCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range jobsChan {
// ...
fmt.Println(item)
}
}()
}
// senders
for i := 0; i < 1000; i++ {
jobsChan <- i
}
// 关闭channel,上游的goroutine在读完channel的内容,就会通过wg的done退出
close(jobsChan)
wg.Wait()
}
通过WaitGroup启动指定数量的goroutine,监听channel的通知。发送者推送信息到channel,信息处理完了,关闭channel,等待goroutine依次退出。
2.使用semaphore
package main
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/semaphore"
)
const (
// 同时运行的goroutine上限
Limit = 3
// 信号量的权重
Weight = 1
)
func main() {
names := []string{
"小白",
"小红",
"小明",
"小李",
"小花",
}
sem := semaphore.NewWeighted(Limit)
var w sync.WaitGroup
for _, name := range names {
w.Add(1)
go func(name string) {
sem.Acquire(context.Background(), Weight)
// ... 具体的业务逻辑
fmt.Println(name, "-吃饭了")
time.Sleep(2 * time.Second)
sem.Release(Weight)
w.Done()
}(name)
}
w.Wait()
fmt.Println("ending--------")
}
25.golang的channel数据结构?
type hchan struct {
qcount uint // 数组长度,即已有元素个数
dataqsiz uint // 数组容量,即可容纳元素个数
buf unsafe.Pointer // 数组地址
elemsize uint16 // 元素大小
closed uint32 // 关闭状态
elemtype *_type // 元素类型
sendx uint // 下一次写下标位置
recvx uint // 下一次读下标位置
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex // 锁
}
26.golang中的垃圾回收机制gc?
标记清除法:
原来:触发gc,暂停业务逻辑stop the word ,找到可达和不可达对象,标记不可达对象 并清除(sweep),停止暂停。
优化:标记清除的GC策略最主要问题在于STW时间过长,优化方向就是减少STW时间,如下图:
即调整GC流程的执行顺序,将Sweep操作挪出STW,使之在和程序业务逻辑并发执行,以此达到缩短STW时间的目的。但是效果有限,前面说到的找到可达对象可标记的过程涉及整个内存空间,占了时间大头。那么优化GC要从标记出发,想方设法地在标记阶段减少STW。
三色标记法: 一次流程过后只剩下白色和黑色 回收白色的
白色 新创建的对象
灰色 中间过程中被引用了的对象
黑色 引用了其他对象的对象
1.开始标记,只要是新创建的对象,默认颜色都是标记为"白色",以上图为例,当前对象1-7都是白色
2.从程序(对象根节点)出发,即从程序出发,开始遍历所有对象,把遍历到的对象从白色集合放入到灰色集合,当前对象1,对象4为灰,其余为白
3.遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将原灰色集合放入黑色集合,当前1,4为黑,2,7为灰,3,5,6为白
4.重复第三步,直到灰色表中无任何对象
5.回收所有白色标记的对象,也就是回收垃圾
**对三色标记法的探讨:**三色标记法的出现是为了优化GC的效率,最重要的一点是缩短STW的时间。
会误删除引用对象
- 一个白色对象当前被黑色对象引用(即白色被挂在黑色下)
- 灰色对象与该白色之间的引用关系被破坏(即灰色同时丢失了该白色)
要想在标记阶段没有STW的存在,就要阻止以上两个条件同时发生的情况。所以在对象引用条件上提出了强三色不变式和弱三色不变式两种规则,分别从上面两个条件出发,防止引用对象被误删除的情况发生。
强制性的不允许黑色对象引用白色对象,破坏条件1。
黑色对象可以引用白色,但同时该白色对象必须存在其他灰色对象对它的引用,或者是在该白色对象引用链路的上游存在灰色对象,破坏条件2。
在三色标记中只要满足强/弱三色不变式的一种,即可保证引用对象不丢失。
屏障机制 实现强/弱三色不变式
插入写屏障
具体操作:在A对象引用B对象的时候,将B对象标记为灰色(如果要将B挂在A的下游,B必须要被标记为灰色)
满足:强三色不变式(不会存在黑色对象引用白色对象的情况,因为白色对象会被强制变为灰色对象)
插入屏障只限制堆上的对象,不限制栈上的对象
我们知道对象存储是在堆上或者是栈上,因为每次插入都要做判断的话会影响性能。如果运行时需要在几百个 Goroutine 的栈上都开启写屏障,会带来巨大的额外开销,又加上栈空间比较小,但是要求相应速度快,因为函数调用弹出频繁使用,所以在栈上是没有插入屏障的,只在堆上有
插入写屏障的不足
因为插入屏障不限制栈上对象的原因,所以在三色标记法标记完整个heap之后,要启动STW,把栈上黑色对象全部置为白色,重新遍历扫描一次栈空间。因为栈空间比较小,所以耗时很短,大约需要10-100ms
删除写屏障
具体操作:被解除引用关系(删除)的对象,如果自身为白色,那么被标记会灰色。(因为黑色不能直接引用白色)
满足:弱三色不变式(保护灰色对象到白色对象的路径不会断)
比如a(黑)b(灰色)同时指向c(白色),现在b(灰色)不再引用c(白色)了,将c(白色)标记为c(灰色)。
删除屏障的不足
回收精度低,一个对象即使被删除了最后一个指向它的指针,它也依旧可以活过这一轮GC,在下一轮GC中才能被清除掉。不过该问题只是影响当前可用内存大小,可以忽略。
最好的 背这个
三色标记法+混合写屏障机制
栈上的可达对象栈上创建的新对象
注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
变形的弱三色不变式。结合了插入,删除写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW,而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。
GC触发的条件。定量、定时、手动、空间不足触发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hW0kr9rH-1668929833213)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220822221530241.png)]
27.go协程池??
28.struct和class的区别?
总结,主要有这么几点不同:
1.struct 是值类型,class 是对象类型。
2.struct 不能被继承,class 可以被继承。
3.struct 默认的访问权限是public,而class 默认的访问权限是private。
4.struct总是有默认的构造函数,即使是重载默认构造函数仍然会保留。这是因为Struct的构造函数是由编译器自动生成的,但是如果重载构造函数,必需对struct中的变量全部初始化。并且Struct的用途是那些描述轻量级的对象,例如Line,Point等,并且效率比较高。class在没有重载构造函数时有默认的无参数构造函数,但是一被重载默认构造函数将被覆盖。
5.struct的new和class的new是不同的。struct的new就是执行一下构造函数创建一个新实例再对所有的字段进行Copy。而class则是在堆上分配一块内存然后再执行构造函数,struct的内存并不是在new的时候分配的,而是在定义的时候分配
29.GMP/GPM调度模型
传统网络模型的缺点,
Process Per Connection
Thread Per Connection
不管是fork进程还是新建线程,都存在较大的系统开销(线程相对更小),于是工程师们想到了PreFork和PreThread网络模型:
PreFork, 即预先创建好进程,新网络连接到来之后直接使用之前新建的进程,而不用临时创建,降低网络请求耗时
PreThread,同理
再以PreThread为例,预先创建线程,并将其放在一起管理即为线程池。这种模式存在另外一个问题,即当线程处理一个网络请求时需要系统调用(IO),线程就会进入阻塞状态,同一时间可用的线程数减少,线程池的处理能力大大降低
纯粹增加线程池中的线程数量可以一定程度上环节这个问题,但是当线程数达到一个阈值后,线程切换将消耗大量的CPU,系统处理能力很快达到瓶颈。
GMP模型
G(Goroutine): 即Golang协程,协程是一种用户态线程,比内核态线程更加轻量。使用go关键字可以创建一个Golang协程
M(Machine): 即工作线程。实质上实现业务逻辑的载体
P(Processor): 处理器。是Golang内部的定义,非CPU。包含运行Go代码的必要资源,也有调度Goroutine的能力
MPGPG
P的个数在程序启动时决定,默认情况下等同于CPU的核数,由于M必须持有一个P才可以运行Go代码,所以同时运行的 M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。
Processor常规调度
每个P维护一个G队列,P周期性的将G调度到M上执行一小段时间,然后保存上下文,并将次G放到队列末尾,继续执行下一个G。
除了每个Processor拥有的G队列以外,Go还维护一个全局G队列(主要是一些从系统调用/IO中恢复的G) ,Processor还会周期的查看全局G队列中时候有就绪的G,有的话就拿到自己的队列中,防止全局队列中的G被“饿死”。