golang八股文整理(持续搬运)

文章目录

    • 1.Go语言——垃圾回收
    • 2.CPM调度和CSP模型
    • 3.chan原理
    • 4.context结构原理
    • 5. 竞态、内存逃逸
    • 6. golang中new和make的区别?
    • 7.Go中对nil的Slice和空Slice的处理是一致的吗?
    • 8.Golang的内存模型中为什么小对象多了会造成GC压力?
    • 9.channel为什么能做到线程安全?
    • 10.GC的触发条件
    • 11.怎么查看Goroutine的数量?怎么限制Goroutine的数量?
    • 12. Channel是同步的还是异步的?
    • 13. Goroutine和线程的区别?
    • 14. Go的Struct能不能比较?
    • 15. Go的Slice如何扩容?
    • 16.在Go函数中为什么会发生内存泄露?发生了泄漏如何检测?
    • 17. Go中两个Nil可能不相等吗?
    • 18.Go语言中的内存对齐
    • 19.两个 interface 可以比较吗?
    • 20.go 打印时 %v %+v %#v 的区别?
    • 21.什么是 rune 类型?
    • 22.空 struct{} 占用空间么?用途是什么?

1.Go语言——垃圾回收

golang八股文整理(持续搬运)_第1张图片
Go V1.3之前的标记-清除:
1.暂停业务逻辑,找到不可达的对象,和可达对象
2.开始标记,程序找出它所有可达的对象,并做上标记
3.标记完了之后,然后开始清除未标记的对象。
4.停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束

标记-清除的缺点:
STW(stop the world):让程序暂停,程序出现卡顿
标记需要扫描整个heap
清除数据会产生heap碎片

为了减少STW的时间,后来对上述的第三步和第四步进行了替换。

Go V1.5 三色标记法
1.把新创建的对象,默认的颜色都标记为“白色”
golang八股文整理(持续搬运)_第2张图片

2.每次GC回收开始,然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合
golang八股文整理(持续搬运)_第3张图片

3.遍历灰色集合,将灰色对象引用的对象从白色集合放入到灰色集合,之后将此灰色对象放入到黑色集合
golang八股文整理(持续搬运)_第4张图片
4.重复第三步,直到灰色中无任何对象
golang八股文整理(持续搬运)_第5张图片
golang八股文整理(持续搬运)_第6张图片
5.回收所有的白色标记的对象,也就是回收垃圾

三色标记法在不采用STW保护时会出现:
1.一个白色对象被黑色对象引用
2.灰色对象与它之间的可达关系的白色对象遭到破坏

这两种情况同时满足,会出现对象丢失

解决方案:
1.强三色不变式:强制性的不允许黑色对象引用白色对象(破坏1)
2.弱三色不变式:黑色对象可以引用白色对象,白色对象存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象(破坏2)

屏障:
1.插入屏障:在A对象引用B对象的时候,B对象被标记为灰色(满足强三色不变式,黑色引用的白色对象会被强制转坏为灰色)。只有堆上的对象触发插入屏障,栈上的对象不触发插入屏障。在准备回收白色前,重新遍历扫描一次栈空间。此时加STW暂停保护栈,防止外界干扰。
golang八股文整理(持续搬运)_第7张图片
不足:结束时需要使用STW来重新扫描栈

2.删除屏障:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色(满足弱三色不变式)。
golang八股文整理(持续搬运)_第8张图片
删除屏障的不足:回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

Go V1.8的三色标记法+混合写屏障机制
具体操作:
1.GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
2.GC期间,任何在栈上创建的新对象,均为黑色
3.被删除对象标记为灰色
4.被添加的对象标记为灰色
满足:变形的弱三色不变式(结合了插入、删除写屏障的优点)

2.CPM调度和CSP模型

CSP模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯channel来进行通信的并发模型。

GPM分别是什么,分别有多少数量?
G:goroutine,go的协程,每个go关键字都会创建一个协程
M:machine,工作线程,在Go中称为Machine,数量对应真实的CPU数
P:process,包含运行Go代码所需要的必要资源,用来调度G和M之间的关联关系,其数量可以通过GOMAXPROCS0来设置,默认为核心数

线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

Goroutine调度策略
1.队列轮转:P会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度,P还会周期性的查看全局队列是否有G等待调度到M中执行
2.系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是M的缓存池,也可能是新建的。
3.当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。
golang八股文整理(持续搬运)_第9张图片

3.chan原理

底层是一个环形队列
结构体:

type hchan struct {
qcount uint // 队列中的总元素个数
dataqsiz uint // 环形队列大小,即可存放元素的个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 //每个元素的大小
closed uint32 //标识关闭状态
elemtype *_type // 元素类型
sendx uint // 发送索引,元素写入时存放到队列中的位置

recvx uint // 接收索引,元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex //互斥锁,chan不允许并发读写
}

从channel中读数据:
1.若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。

2.如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。

3.如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。.将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒

从channel中写数据

1.若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。

2.若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。

3.若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。

关闭 channel

关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。

4.context结构原理

Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

「Deadline」 方法:可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。
「Done」 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。
「Err」 方法:返回Context 被取消的原因。
「Value」 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

5. 竞态、内存逃逸

1.资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争。

解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。

sync.Mutex
sync.RWMutex

2.逃逸分析就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

逃逸场景:

指针逃逸
栈空间不足逃逸
动态类型逃逸
闭包引用对象逃逸

6. golang中new和make的区别?

1.make 仅用来分配及初始化类型为 slice、map、chan 的数据。
2.new 可分配任意类型的数据,根据传入的类型申请一块内存,返回指向这块内存的指针,即类型 *Type。
3.make 返回引用,即 Type,new 分配的空间被清零, make 分配空间后,会进行初始。

7.Go中对nil的Slice和空Slice的处理是一致的吗?

首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。
1.slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的。
2.slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

8.Golang的内存模型中为什么小对象多了会造成GC压力?

通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。

9.channel为什么能做到线程安全?

channel可以理解是一个先进先出的循环队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。

10.GC的触发条件

1.主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。
2.被动触发,分为两种方式:

2.1.使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。
2.2.使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。

11.怎么查看Goroutine的数量?怎么限制Goroutine的数量?

1.在Golang中,GOMAXPROCS中控制的是未被阻塞的所有Goroutine,可以被 Multiplex 到多少个线程上运行,通过GOMAXPROCS可以查看Goroutine的数量。
2.使用通道。每次执行的go之前向通道写入值,直到通道满的时候就阻塞了

12. Channel是同步的还是异步的?

Channel是异步进行的, channel存在3种状态:

1.nil,未初始化的状态,只进行了声明,或者手动赋值为nil
2.active,正常的channel,可读或者可写
3.closed,已关闭,千万不要误认为关闭channel后,channel的值是nil
golang八股文整理(持续搬运)_第10张图片

13. Goroutine和线程的区别?

1.一个线程可以有多个协程
2.线程、进程都是同步机制,而协程是异步
3.协程可以保留上一次调用时的状态,当过程重入时,相当于进入了上一次的调用状态
4.协程是需要线程来承载运行的,所以协程并不能取代线程,「线程是被分割的CPU资源,协程是组织好的代码流程」

14. Go的Struct能不能比较?

1.相同struct类型的可以比较
2.不同struct类型的不可以比较,编译都不过,类型不匹配

15. Go的Slice如何扩容?

1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
2.否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍。
3.否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1.25倍。
4.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

16.在Go函数中为什么会发生内存泄露?发生了泄漏如何检测?

Goroutine 需要维护执行用户代码的上下文信息,在运行过程中需要消耗一定的内存来保存这类信息,如果一个程序持续不断地产生新的 goroutine,且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。
可以通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源。

17. Go中两个Nil可能不相等吗?

Go中两个Nil可能不相等。

接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

两个接口值比较时,会先比较 T,再比较 V。接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较

func main() {
 var p *int = nil
 var i interface{} = p
 fmt.Println(i == p) // true
 fmt.Println(p == nil) // true
 fmt.Println(i == nil) // false
}

18.Go语言中的内存对齐

CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度,内存访问粒度跟机器字长有关。

对齐规则:
1.结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度或当前成员变量类型的长度,取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
2.结构体本身,对齐值必须为编译器默认对齐长度,或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
3.结合以上两点,可得知若编译器默认对齐长度,超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的

19.两个 interface 可以比较吗?

1.判断类型是否一样
reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind()

2.判断两个interface{}是否相等
reflect.DeepEqual(a, b interface{})

3.将一个interface{}赋值给另一个interface{}
reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))

20.go 打印时 %v %+v %#v 的区别?

%v 只输出所有的值;
%+v 先输出字段名字,再输出该字段的值;
%#v 先输出结构体名字值,再输出结构体(字段名字+字段的值);

package main
import "fmt"

type student struct {
 id   int32
 name string
}

func main() {
 a := &student{id: 1, name: "微客鸟窝"}

 fmt.Printf("a=%v \n", a) // a=&{1 微客鸟窝} 
 fmt.Printf("a=%+v \n", a) // a=&{id:1 name:微客鸟窝} 
 fmt.Printf("a=%#v \n", a) // a=&main.student{id:1, name:"微客鸟窝"}
}

21.什么是 rune 类型?

Go语言的字符有以下两种:

1.uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
2.rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

22.空 struct{} 占用空间么?用途是什么?

空结构体 struct{} 实例不占据任何的内存空间。

用途:
1.将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。
2.不发送数据的信道(channel)
使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。
3.结构体只包含方法,不包含任何的字段

你可能感兴趣的:(go语言,golang)