本文介绍: 1.newmake区别两者的作用类型不同makeslicemapchannel分配内存newintstring数组结构分配内存。返回类型不一样,new返回指向变量指针make返回变量本身;new 分配空间被清零(也就是变为该类型的零值数值零值为0、string零值为””、bool零值false指针/结构体的零值为nil。)。make 分配空间后,会对切片容量,管道缓存长度基于键值对的无序集合容量等进行初始化;一般来说,make会在栈上开辟一块栈帧,

1.newmake区别

两者的作用类型不同makeslicemapchannel分配内存;newintstring数组结构分配内存。

返回类型不一样,new返回指向变量指针,make返回变量本身;

new 分配的空间被清零(也就是变为该类型的零值。数值型零值为0、string的零值为””、bool的零值为false指针/结构体的零值为nil。)。make 分配空间后,会对切片容量,管道缓存长度基于键值对的无序集合容量等进行初始化

一般来说,make会在栈上开辟一块栈帧,栈帧里面有栈的指针和栈顶指针,分别记录栈帧的空间,随着函数执行完毕,栈里的栈帧就会自动清空;一般来说,当指向这个内存空间的指针变量作用域不会在作用域外被使用,或者说这个变量使用一次就不再使用。那么new分配的内存空间就会在当前函数栈中随着栈的结束而被销毁。具体的话要对CPU内存的逃逸分析,结构体大传结构体指针作为参数,此时是在堆上分配内存,指针类型相比于值类型更节省内存空间。结构体小传结构体,在栈上分配内存,有效减轻GC压力。编译期不能确定变量的值(或者fmt返回接口类型)/变量占用内存空间太大/函数返回一个函数声明变量地址(非法内存)会逃逸到堆。

2、数组切片的区别

1)定义方式不一样

数组定义

var a1 [3]int

var a2 […]int{1,2,3}

切片定义

var a1 []int

var a2 :=make([]int,3,5)

2)初始化方式不一样,数组需要指定大小大小不改变 

数组初始化

a1 := […]int{1,2,3}

a2 := [5]int{1,2,3}

切片初始化

b:= make([]int,3,5)

3、for range时候它的地址会发生变化么?

不会。在 for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环遍历到的数据都是以值覆盖方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程解决办法:在每次循环时,创建一个临时变量接收a/b的值,再把临时变量的地址传给协程

4、go defer多个 defer顺序defer什么时机会修改返回值

避坑指南defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露

多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中

deferreturnreturn value函数返回值执行顺序:首先return,其次return value最后deferdefer可以修改函数最终返回值修改时机:有名返回值或者函数返回指针 

5、uint 类型溢出问题

var a uint8 =255(0~255,2的八次方)

266就溢出了,数据不准

6、能介绍rune 类型吗?

byte 等同于int8,常用来处理ascii字符

rune 等同于int32,常用来处理unicodeutf-8字符

golang中的字符串底层实现通过byte数组的,中文字符unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8

计算字符串长度len([]rune(str)

7、 golang解析 tag 是怎么实现的?反射原理什么

Go解析tag通过反射实现的,反射是指计算机程序运行时(Run time可以访问检测修改它本身状态行为的一种能力动态知道给定数据对象的类型和结构,并有机会修改它。反射接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改tag是啥:结构体支持标记name string `json:namefield` 就是 `json:namefield` 这部分

gorm json yaml gRPC protobuf gin.Bind()都是通过反射来实现

8、调用函数传入结构体时,应该传值还是指针?

Go 的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数地址传递到函数中,那么在函数中参数所进行的修改,将影响到实际参数

9、讲讲 Goslice 底层数据结构和一些特性

因为 Go 里面的 mapslicechan 是引用类型。变量区分值类型和引用类型。所谓值类型:变量和变量的值存在同一个位置。所谓引用类型:变量和变量的值是不同位置,变量的值存储的是对值的引用地址

Go 的 slice 底层数据结构是由一个 array 指针指向底层数组len 表示切片长度cap 表示切片容量。slice 的主要实现扩容。对于 appendslice 添加元素时,假如 slice 容量够用,则追加元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice扩容扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。(关于刚才问的 slice 为什么传到函数内可能被修改,如果 slice 在函数内没有出现扩容,函数外和函数内 slice 变量指向同一个数组,则函数内复制的 slice 变量值出现更改,函数外这个 slice 变量值也会被修改。如果 slice 在函数内出现扩容,则函数内变量的值会新生成个数组(也就是新的 slice,而函数外的 slice 指向还是原来的 slice,则函数内的修改不会影响函数外的 slice。slice在append时候,如果底层数组的大小(cap)不够了,就会发生扩容。发生扩容的时候,slice结构体的指针会指向一个新的底层数组,然后把原来数组中的元素拷贝到新数组中,最后添加append的新元素,就完成了扩容。
所以在这个时候,函数内部slice的改变是不会影响到函数外部slice的。因为此时,两个结构体中的指针指向的底层数组已经不相同了。)

10、讲讲 Go 的 select 底层数据结构和一些特性

go 的 selectgolang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否读写事件是否 readylinux系统 IO 模型selectpoll,epollgo 的 selectlinux 系统 select 非常相似

select特性

每个case语句都必须对应channel的读写操作,select语句会陷入阻塞,直到一个或者多个channel可以读写才能恢复

1)select 操作至少要有一个 case 语句出现读写 nilchannel分支忽略,在 nilchannel操作则会报错

2)select支持管道,而且是单协程操作

3)每个 case 语句仅能处理一个管道,要么要么写。

4)多个 case 语句执行顺序随机的。

5)存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能

11、讲讲 Go 的 defer 底层数据结构和一些特性

答:每个 defer 语句都对应一个_defer 实例多个实例使用指针连接起来形成一个单连表保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入链表头部,函数结束一次头部取出,从而形成后进先出的效果

defer 的规则总结

延迟函数的参数defer 语句出现时候就已经确定了的。

延迟函数执行按照后进先出的顺序执行,即先出现defer 最后执行

延迟函数可能操作主函数的返回值

申请资源后立即使用 defer 关闭资源是个好习惯。

12、单引号双引号,反引号的区别?

单引号表示byte类型或rune类型,对应 uint8和int32类型,默认rune 类型。byte用来强调数据raw data,而不是数字;而rune用来表示Unicodecode point。

双引号,才是字符串,实际上是字符数组。可以用索引访问字节,也可以用len()函数来获取字符串所占的字节长度

引号表示字符字面量,但不支持任何转义序列字面raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符

二、map相关
1、map 使用注意的点,是否并发安全
map的类型是map[key],key类型必须是可比较的,通常情况,会选择内建的基本类型,比如整数字符串做key的类型。如果要使用struct作为key,要保证struct对象在逻辑上是不可变的。在Go语言中,map[key]函数返回结果可以是一个值,也可以是两个值。map无序的,如果我们想要保证遍历map时元素有序,可以使用辅助数据结构例如orderedmap

第一,一定要先初始化,否则panic

第二,map类型是容易发生并发访问问题的。不注意就容易发生程序运行并发读写导致的panic。 Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查遇到并发问题就会导致panic。

2、map 循环有序还是无序的?

无序的, map 因扩张⽽重新哈希时,各键值项存储位置可能会发生改变,顺序自然也没法保证了,所以官方避免大家依赖顺序直接打乱处理。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种

3、 map删除一个 key,它的内存会释放么?(常问)

如果删除的元素是值类型,如int,floatboolstring以及数组和struct,map的内存不会自动释放

如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用

将map设置nil后,内存被回收

4、怎么处理对 map 进行并发访问?有没有其他方案? 区别是什么

方式一、使用内置sync.Map

方式二、使用读写锁实现并发安全map

5、 nil map 和空 map 有何不同

1)可以对未初始化的map进行取值,但取出来的东西是空:

var m1 map[string]string

fmt.Println(m1[“1”])

2)不能对未初始化的map进行赋值,这样将会抛出一个异常

var m1 map[string]string

m1[“1”] = “1”

panic: assignment to entry in nil map

3) 通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断

nil map 未初始化,空map是长度为空

6、map 的数据结构什么?是怎么实现扩容?
答:golang 中 map 是一个 kv集合。底层使用 hash table,用链表解决冲突 ,出现冲突时,不是每一个 key 都申请一个结构通过链表串起来,而是以 bmap最小粒度挂载,一个 bmap 可以放 8 个 kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个 map 的底层结构是 hmap,是有若干个结构为 bmapbucket 组成的数组。每个 bucket 底层都采用链表结构。

map 的容量大小

底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子计算公式是:装载因子=填入表中的元素个数/散列表长度,装载因子越大,说明空闲位置越少,冲突越多,散列表性能会下降。底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子计算公式是:装载因子=填入表中的元素个数/散列表长度,装载因子越大,说明空闲位置越少,冲突越多,散列表性能会下降。

触发 map 扩容的条件

1)装载因子超过阈值源码里定义的阈值是 6.5。

2)overflowbucket 数量过多 map 的 bucket 定位和 key 的定位高八位用于定位 bucket,低八位用于定位 key,快速试错后再进行完整对比

7、slices能作为map类型的key吗?

当时被问的一脸懵逼,其实是这个问题的变种:golang 哪些类型可以作为map key?

答案是:在golang规范中,可比较的类型都可以作为map key;这个问题又延伸到在:golang规范中,哪些数据类型可以比较

不能作为map key 的类型包括:

slices

maps

functions

三、context相关

1、context 结构是什么样的?context 使用场景用途

答:Go 的 Context数据结构包含 Deadline,Done,Err,Value,Deadline 方法返回一个 time.Time,表示当前 Context 应该结束时间ok表示结束时间,Done 方法当 Context取消或者超时时候返回的一个 closechannel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。context业务中是经常被使用的,

其主要的应用

1:上下文控制,2:多个 goroutine 之间数据交互等,3:超时控制:到某个时间点超时,过多久超时。

四、channel相关

1、channel 是否线程安全?锁用在什么地方?

是。在对buf循环队列中的数据入队出队操作互斥锁Mutex保证读写安全

2、go channel 的底层实现原理 (数据结构)

buf发送队列接收队列lock

3、nil、关闭channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型,重要)

1. 对已关闭通道进行写操作

看下面代码输出什么结果?

func main() {

 ch := make(chan string, 1)

 close(ch)

 ch <- “hello, world

 fmt.Println(<-ch)

}

输出结果为:

panic: send on closed channel

这个结果无论对于非缓冲通道还是缓冲通道都是一样的,即对已经关闭通道进行写操作,会触发 panic 。

2. 对已关闭通道进行读操作

对已经关闭channel 进行读操作要分为两种情况:

关闭的 channel 没有数据;

关闭的 channel 仍然有数据;

2.1 已关闭的 channel 没有数据

func main() {

 ch := make(chan int, 1)

 close(ch)

 // 第一次从通道中获取数据

 value, ok := <-ch

 fmt.Println(value) // 0

 fmt.Println(ok) // false

 // 第二次从通道中获取数据

 fmt.Printf(“<-ch is %vn”, <-ch) // <-ch is 0

 fmt.Println(ok) // false

}

从上面代码结果我们可以得出结论,对于已经关闭的 channel ,如果通道内已经没有数据,那么可以不限次数的进行读取操作,但是读到的值是该 channel 类型的零值,也就是上面代码中 int 的零值 0。

如果该通道被声明为 string 类型,那么其零值就为空字符串 “”。

而从通道返回的第二个值,也就是标志位 ok,则会一直为 false

2.2 已关闭的 channel 仍然有数据

func main() {

 ch := make(chan int, 3)

 ch <- 1

 close(ch)

 // 第一次从通道中获取数据

 value, ok := <-ch

 fmt.Println(“第一次从通道中获取数据 value“, value) // 1

 fmt.Println(“第一次从通道中获取数据 ok”, ok) // true

 // 第二次从通道中获取数

 value, ok = <-ch

 fmt.Println(“第二次从通道中获取数value”, value) // 0

 fmt.Println(“第二次从通道中获取数据 ok”, ok) // false

}

对于这种情况,从通道里面仍然可以获取到之前存储的数据,同时第二个返回值 ok 的值为 true, 表示通道里面是有数据的。

但是当通道里面的值被取完时,就和上面 2.1 小节的结果一样了。

关闭nil管道会panic,读写nil管道会阻塞

打开但空的管道/写打开但满了的管道,阻塞

只读的管道/读只写/关闭只写的管道,编译错误

关闭已关闭的管道,panic

关闭打开了的有数据的管道,成功关闭,直到管道无值,然后取出默认值

打开但空,成功关闭,取出零值

4、向 channel 发送数据和从 channel 读数据的流程什么样的?(难点)

channel是内置的一种数据类型用于两个go程中间数据的传输

go语言的channel采用的是环形缓存环形缓存的内存空间可以复用,减少了GC的压力。
 

首先是channel分为两类:

1)无缓冲channel,可以看作“同步模式”,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(后面会看到,实际上就是内存拷贝)。否则,任意一方先行进行发送接收操作,都会被挂起,等待另一方的出现才能被唤醒

2)有缓冲channel称为异步模式”,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作 (如接收)才会被唤醒

1. Channel本质上是由三个FIFO(First In FirstOut,先进先出)队列组成的用于协程之间传输数据协程安全的通道;FIFO的设计是为了保障公平,让事情变得简单原则是让等待时间最长的协程最有资格先从channel发送或接收数据;

2. 三个FIFO队列依次是buf循环队列,sendq待发送者队列recvq待接收者队列。buf循环队列是大小固定用来存放channel接收的数据的队列;sendq待发送者队列,用来存放等待发送数据到channel的goroutine双向链表,recvq待接收者队列,用来存放等待从channel读取数据goroutine的双向链表;sendq和recvq可以认为不限大小;

3. 跟函数调用传参本质都是传值一样,channel传递数据本质就是值拷贝,引用类型数据的传递也是地址拷贝;有从缓冲区buf地址拷贝数据到接收者receiver栈内存地址,也有从发送者sender栈内存地址拷贝数据到缓冲区buf

4. Channel里面参数的修改不是并发安全的,包括对三个队列及其他参数的访问,因此需要加锁本质上,channel就是一个有锁队列;

5. Channel 的性能sync.Mutex 差不多,没有谁比谁强。Go官方之所以推荐使用Channel进行并发协程的数据交互,是因为channel的设计理念能让程序变得简单,在大型程序、高并发复杂运行状况中也是如此。

goroutine发送给channel(写channel):发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。即:进入通道的并不是操作符右边的那个元素值,而是它的副本

goroutine】接收给channel(读channel):接收操作包含了“复制通道内的元素值”、“放置副本到接收方”、“删掉原值”三个步骤

关闭:关闭 channel 会产生一个广播机制,所有向 channel 读取消息goroutine 都会收到消息

向 channel 写数据:

如果等待接收队列 recvq 不为空说明缓冲区中没有数据或者没有缓冲区,此时直接recvq 取出 G,并把数据写入最后把该 G 唤醒,结束发送过程

如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;

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

从 channel 读数据

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

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

如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。

如果缓冲区中没有数据,将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。

5、讲讲 Go 的 chan 底层数据结构和主要使用场景

channel 的数据结构包含

qccount 当前队列中剩余元素个数

dataqsiz 环形队列长度即可以存放的元素个数,

buf 环形队列指针,

elemsize 每个元素的大小,

closed 标识关闭状态

elemtype 元素类型,

sendx 队列下表,指示元素写入时存放到列中位置

recv 队列下表,指示元素从队列的该位置读出。

recvq 等待读消息goroutine 队列,

sendq 等待写消息的 goroutine 队列,

lock 互斥锁,

chan 不允许并发读写。

无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据

channel 的一些特点

1)、读写值 nil 管道会永久阻塞

2)、关闭的管道读数据仍然可以读数据

3)、往关闭的管道写数据会 panic

4)、关闭为 nil 的管道 panic

5)、关闭已经关闭的管道 panic

使用场景消息传递消息过滤信号广播事件订阅与广播,请求响应转发任务分发,结果汇总,并发控制限流,同步与异步

五、GMP相关

1、什么是 GMP?(必问)

答:G 代表着 goroutine,P 代表上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入全局队列中。

关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议逻辑 CPU 数量的 2 倍,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置设置好 P,M 一般都是要大于 P。

2、进程线程、协程有什么区别?(必问)

进程:是应用程序启动实例,每个进程都有独立的内存空间不同进程通过进程间的通信方式通信

线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。

协程:为轻量级线程,与线程相比,协程不受操作系统调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行

3、抢占式调度是如何抢占的?

基于协作式抢占

基于信号量抢占

就像操作系统负责线程的调度一样,Go的runtime负责goroutine的调度。现代操作系统调度线程都是抢占式的,我们不能依赖用户代码主动让出CPU,或者因为IO、锁等待而让出,这样会造成调度的不公平。基于经典的时间片算法,当线程的时间片用完之后,会被时钟中断给打断,调度器会将当前线程的执行上下文进行保存,然后恢复下一个线程的上下文,分配新的时间片令其开始执行。这种抢占对于线程本身是无感知的,系统底层支持,不需要开发人员特殊处理

基于时间片的抢占式调度有个明显的优点,能够避免CPU资源持续被少数线程占用,从而使其他线程长时间处于饥饿状态。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,因为整个Go程序都是运行用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量

4、M 和 P 的数量问题?

p默认cpu内核

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能创建很多个M出来

六、锁相关

1、除了 mutex 以外还有那些方式安全读写共享变量?

共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。

可以用个数为 1 的信号量semaphore)实现互斥

通过 Mutex 锁实现

2、Go 如何实现原子操作?

答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态要么看到原子操作已经完成要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU 绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。

Go 语言标准库代码包 sync/atomic 提供了原子的读取(Load前缀的函数)或写入(Store前缀的函数)某个值。

原子操作与互斥锁的区别

1)、互斥锁是一种数据结构,用来让一个线程执行程序的关键部分完成互斥的多个操作。

2)、原子操作是针对某个值的单个互斥操作。

3、Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?

悲观

悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接该数据进行加锁以防止并发。这种借助数据库机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

乐观

乐观锁是相对悲观锁而言的,乐观假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量

4、Mutex 有几种模式?

1)正常模式

当前的mutex只有一个goruntine来获取,那么没有竞争直接返回。

新的goruntine进来,如果当前mutex已经被获取了,则该goruntine进入一个先入先出的waiter队列,在mutex被释放后,waiter按照先进先出的方式获取锁。该goruntine会处于自旋状态(不挂起,继续占有cpu)。

新的goruntine进来,mutex处于空闲状态,将参与竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。

2)饥饿模式

在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。 如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式:

waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;

此 waiter 的等待时间小于 1 毫秒

5、goroutine 的自旋占用资源如何解决

自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。

自旋的条件如下

1)还没自旋超过 4 次,

2)多核处理器

3)GOMAXPROCS > 1,

4)p 上本地 goroutine 队列为空

mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。

七、并发相关

1、怎么控制并发数?

第一,有缓冲通道

根据通道中没有数据时读取操作陷入阻塞和通道已满时继续写入操作陷入阻塞的特性,正好实现控制并发数量。

2、多个 goroutine 对同一个 map 写会 panic,异常是否可以用 defer 捕获

可以捕获异常,但是只能捕获一次,Go语言,可以使用多值返回来返回错误。不要用异常代替错误,更不要用来控制流程。在极个别的情况下,才使用Go中引入的Exception处理defer, panic, recover Go中,对异常处理的原则是:多用error包,少用panic

3、如何优雅的实现一个 goroutine 池

百度手写代码,本人面传音控股被问道:请求数大于消费能力怎么设计协程池)

高并发系统稳定性、高可用的核心部分之一

八、GC相关

1、go gc 是怎么实现的?(必问)

GC机制随着golang版本变化如何变化的?

三色标记法的流程

插入屏障删除屏障,混合写屏障(具体的实现比较描述,但你要知道屏障作用:避免程序运行过程中,变量被误回收;减少STW的时间)

你觉得以后GC机制会怎么优化

2、go 是 gc 算法是怎么实现的? (得物,出现频率低)

3、GC 中 stw 时机,各个阶段如何解决的? 

4、GC 的触发时机?

初级必问,分为系统触发和主动触发

1)gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。

2)gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。时间周期以runtime.forcegcperiod 变量为准,默认 2 分钟

3)gcTriggerCycle:如果没有开启 GC,则启动 GC。

4)手动触发的 runtime.GC 方法

九、内存相关

1、谈谈内存泄露,什么情况下内存会泄露?怎么定位排查内存泄漏问题?

答:go 中的内存泄漏一般都是 goroutine 泄漏,就是 goroutine 没有被关闭,或者没有添加超时控制,让 goroutine 一只处于阻塞状态,不能被 GC。

内存泄露有下面一些情况

1)如果 goroutine 在执行时被阻塞而无法退出,就会导致 goroutine 的内存泄漏,一个 goroutine 的最低栈大小为 2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。

2)互斥锁未释放或者造成死锁会造成内存泄漏

3)time.Ticker 是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用 stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间

4)字符串的截取引发临时性的内存泄漏

5)切片截取引起子切片内存泄漏

6)函数数组传参引发内存泄漏【如果我们在函数传参的时候用到了数组传参,且这个数组够大(我们假设数组大小为 100 万,64 位机上消耗的内存约为 800w 字节,即 8MB 内存),或者该函数短时间内被调用 N 次,那么可想而知,会消耗大量内存,对性能产生极大的影响,如果短时间内分配大量内存,而又来不及 GC,那么就会产生临时性的内存泄漏,对于高并发场景相当可怕。】

排查方式:

一般通过 pprof 是 Go 的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是 CPU 使用情况、内存使用情况、goroutine 运行情况等,当需要性能调优或者定位 Bug 时候,这些记录信息是相当重要

2、知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?(必问)

答:1)本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。2)栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。3)变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销。编程语言不断优化 gc 算法,主要目的都是为了减少 gc 带来的额外性能开销,变量一旦逃逸会导致性能开销变大。

内存逃逸的情况如下

1)方法内返回局部变量指针。

2)向 channel 发送指针数据。

3)在闭包中引用包外的值。

4)在 slice 或 map 中存储指针。

5)切片(扩容后)长度太大。

6)在 interface 类型上调用方法。

3、请简述 Go 是如何分配内存的?

mcache mcentral mheap mspan

Go 程序启动的时候申请一大块内存,并且划分 spans,bitmap,areana 区域;arena 区域按照页划分一个个小块,span 管理一个或者多个页,mcentral 管理多个 span现场申请使用;mcache 作为线程私有资源,来源于 mcentral。

4、Channel 分配在栈上还是堆上?哪些对象分配在堆上,哪些对象分配在栈上?

Channel 被设计用来实现协程间通信组件,其作用域和生命周期不可能仅限于某个函数内部,所以 golang 直接将其分配在堆上

知道变量的存储位置确实和效率编程关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

如果一个变量被取地址,那么它就有可能被分配到堆上,然而,还要对这些变量做逃逸分析,如果函数 return 之后,变量不再被引用,则将其分配到栈上。

5、介绍一下大对象小对象,为什么小对象多了会造成 gc 压力?
小于等于 32k 的对象就是小对象,其它都是大对象。一般小对象通过 mspan 分配内存;大对象则直接由 mheap 分配内存。通常小对象过多会导致 GC 三色法消耗过多的 CPU。优化思路是,减少对象分配。

小对象:如果申请小对象时,发现当前内存空间存在空闲跨度时,将会需要调用 nextFree 方法获取新的可用的对象,可能会触发 GC 行为

大对象:如果申请大于 32k 以上的大对象时,可能会触发 GC 行为

十、其他问题
1、Go 多返回值怎么实现的?
答:Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器

2、讲讲 Go 中主协程如何等待其余协程退出?

答:Go 的 sync.WaitGroup 是等待一组协程结束,sync.WaitGroup 只有 3 个方法,Add()是添加计数,Done()减去一个计数,Wait()阻塞直到所有的任务完成。Go 里面还能通过有缓冲的 channel 实现其阻塞等待一组协程结束,这个不能保证一组 goroutine 按照顺序执行,可以并发执行协程。Go 里面能通过无缓冲的 channel 实现其阻塞等待一组协程结束,这个能保证一组 goroutine 按照顺序执行,但是不能并发执行。

代码中Add()是什么意思。Add()表示协程计数,可以一次Add多个,如Add(3),可以多次Add(1);然后每个子协程必须调用done(),这样才能保证所有子协程结束,主协程才能结束。

3、Go 语言中不同的类型如何比较是否相等?
答:像 string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较,像 slice,struct,map 则一般使用 reflect.DeepEqual 来检测是否相等。

4、Go 中 init 函数的特征?
答:一个包下可以有多个 init 函数,每个文件也可以有多个 init 函数。多个 init 函数按照它们的文件名顺序逐个初始化应用初始化时初始工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后main 包。不管包被导入多少次,包内的 init 函数只会执行一次。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后main 包。但包级别变量的初始化先于包内 init 函数的执行。

5、Go 中 uintptr 和 unsafe.Pointer 的区别?

答:unsafe.Pointer 是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换uintptr。uintptr 是指针运算工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer 是指针对象进行运算(也就是 uintptr)的桥梁

6、golang共享内存互斥锁)方法实现发送多个get请求

7、从数组中取一个相同大小的slice有成本吗?

或者这么问:从切片中取一个相同大小的数组有成本吗?

 

 

原文地址:https://blog.csdn.net/qq_61801222/article/details/129709176

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_37038.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注