1、golang 中 make 和 new 的区别?(基本必问)
1 .作用变量类型不同,new可以给任意类型分配
内存,make给slice,map,channel分配内存;
2 .返回类型不一样,new返回指向变量的指针,
make返回变量类型本身;
3 .内存操作不一样,new分配空间后内存被清零。
make 分配空间后会进行内存初始化;
2、数组和切片的区别 (基本必问)
1 .内部结构
数组在内存中是一段连续的内存空间,元素的类型
和长度都是固定的。切片在内存中由一个指向底层
数组的指针、长度和容量组成,长度表示切片当前
包含的元素个数,容量表示切片可以容纳的最多元
素个数
2 .长度
数组的长度在创建时指定,不能更改。切片的长度
可以动态扩展或收缩,可以根据需要自由调整。
3 .使用方式
数组在使用时需要明确指定下标访问元素,不能动
态生成。切片可以使用 append 函数向其末尾添
加元素,可以使用 copy 函数复制切片,也可以
使用 make 函数创建指定长度和容量的切片
3、for range 的时候它的地址会发生变化么?for 循环遍历 slice 有什么问题?
1 .地址没有发生变化-所有循环出来的值共用
2 .在使用 for range 语句遍历切片或数组时,
每次迭代都会返回该元素的副本,而不是该元素
的地址。这意味着每次循环完成后,该元素所对
应的地址都是相同的即最初创建的( 因为共用一个
变量所以后面的总会覆盖前面的值) ,并不会改变
3 .在for range中,迭代变量会被重复使用,而
不是在每次迭代中创建一个新的变量,这个设计
选择有助于减少内存分配和提高性能.因此在循
环结束后,可能会遇到保存的值都是最后一次迭
代的值的情况.如果你需要在循环中保存每次迭
代的值,可以考虑在每次迭代中创建一个新的变
量来保存值而不是直接使用迭代变量。
//错误的写法
for _, v := range studs {
gl[ v.Name] = & v
}
//正确的写法
for _, v := range studs {
temp := v
gl[ v.Name] = & temp
}
4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?(for defer)defer recover 的问题?(主要是能不能捕获)
1 .在Go语言中,defer关键字用于延迟执行一个
函数调用,通常用于确保在函数执行结束后释放
资源或执行一些清理操作。多个defer语句按照
后进先出(Last In, First Out,LIFO)的
顺序执行,即最后一个defer语句会最先执行,
倒数第二个会在倒数第一个之后执行,以此类推。
2 .defer 在何时修改返回值: defer语句中
的函数调用是在包含它的函数执行完毕之后才执
行的。如果包含defer的函数有命名的返回值,
并且在defer语句执行时修改了这个返回值,那
么最终的返回值将是defer语句中修改后的值。
func example( ) ( result int) {
defer func ( ) {
result += 10
} ( )
return 5
}
最终的返回值是15。
3 .defer与recover通常一起使用,用于处理
函数中的错误。recover只能捕获在同一个
goroutine中发生的panic,而且必须在defer
中调用。如果recover在没有发生panic的情况
下调用,它会返回nil
4 .defer和recover是Go语言中用于处理资源
释放和错误恢复的重要机制。
5、 uint 类型溢出
Golang的uint类型溢出问题通常会在大量运算
中发生,特别是在涉及到大量循环或者大数运算时
当uint类型的值超过其最大值时,它会发生溢出,
然后从该类型的最小值开始循环,解决方案:
1 .使用更大的数据类型:例如,如果你正在使用
uint32,你可以尝试升级到uint64。这将提供
更大的值范围,从而减少溢出的可能性。
2 .添加溢出检查:在每次运算之后,你可以检查
结果是否小于任一操作数(假设我们只在正数上
进行操作)。如果是这样,那么就发生了溢出。
3 .使用 math/big 包:对于非常大的数值,你
也可以考虑使用 math/big 包中的 Int 类型
这个类型可以处理任意大小的数值,但是运算速
度会慢一些。
6、介绍 rune 类型
1 .相当int32,它用来区分字符值和整数值。
2 .golang中的字符串底层实现是byte数组,
中文字符在unicode下占2个字节,在utf-8占
3 个字节,而golang默认编码正好是utf-8
3 .byte 等同于int8,常用来处理ascii字符
4 .rune 等同于int32,常用来处理unicode或
utf-8字符
func main ( ) {
str := "米虫 is cool"
fmt.Println( "STR LEN - " , len( [ ] rune( str)) )
}
7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)
计算机程序在运行时(Run time)可以访问、检
测和修改它本身状态或行为的一种能力或动态知道
给定数据对象的类型和结构,并有机会修改它。
8、调用函数传入结构体时,应该传值还是指针? (Golang 都是值传递)
1 .结构体的大小:如果结构体非常大,使用指针传
递会更有效率,因为这样只会复制指针值(一般是
8 字节),而不是复制整个结构体。如果结构体小,
值传递和指针传递的性能差异可以忽略不计
2 .是否需要修改原始结构体:如果需要在函数中
修改原始结构体,应该使用指针传递。如果使用
值传递,函数会接收结构体的一个副本,在函数
中对结构体的修改不会影响到原始的结构体。
9、Slice 介绍
type slice struct {
array unsafe.Pointer
len int
cap int
}
array 指向底层数组的指针
len 表示切片长度
cap 表示切片容量
2 .make创建slice时可以指定其长度和容量,
底层会分配一个数组,数组的长度即容量。
slice = make( [ ] int,5,10) 表示该slice
长度为5,容量为10。使用数组来创建slice时
slice与原数组共用一部分内存。
3 .在使用append向slice追加元素时,若slice
空间不足则会发生扩容,扩容会重新分配一块更大
的内存,将原slice拷贝到新slice,再将数据追
加进去然后返回新slice。扩容操作只针对容量,
扩容后的slice长度不变
4 .使用copy内置函数拷贝两个切片时,会将源切
片的数据逐个拷贝到目的切片指向的数组中,拷贝
数量取两个切片长度的最小值.也就是说copy不
会发生扩容.根据数组或切片来生成新的切片一般
使用slice := array[ start:end] 方式,这
种新生成的切片没有指定容量,新切片的容量是从
start开始到array的结束( 注意并不是到 end)
另一种写法,生成新切片同时指定其容量:
slice[ start:end:cap] ,其中的 cap 为新
切片的容量,容量不能超过原切片实际值。
5 .创建切片可根据实际需要预分配容量,尽量
避免追加过程中扩容操作,有利于提升性能.
10、go struct 能不能比较?
在Go语言中,结构体( Struct) 是否可以比较取决
于其字段。如果结构体的所有字段都是可比较的
那么这个结构体也是可比较的。这意味着你可以
使用 == 或 != 运算符来比较两个结构体变量。
11、context 结构是什么样的?
type Context interface {
Done ( ) <- chan struct { }
Err ( ) error
Deadline ( ) ( deadline time. Time, ok bool )
Value ( key interface { } ) interface { }
}
Context是一个接口, 定义了4 个方法, 它们都是
幂等的。也就是说连续多次调用同一个方法,
得到的结果都是相同的。
1. Done ( ) 方法当 Context被取消或者超时时候
返回的一个close 的channel, 告诉给context
相关的函数要停止当前工作, 然后返回了
2. Err ( ) 返回一个错误, 表示channel被关闭
的原因例如是被取消,还是超时。
3. Deadline ( ) 方法返回一个 time. Time,表示
当前 Context 应该结束的时间,ok 则表示有结
束时间,
4. Value ( ) 获取之前设置的key对应的value。
12、context 使用场景和用途?(基本必问)
context提供了一种在不同goroutine之间传递
取消信号、截止时间、截止日期等元数据的方法。
context主要用于在大规模的并发或分布式系统
中进行取消操作、截止时间管理、跟踪请求以及
传递其他请求范围的数据。以下是一些context
的常见使用场景和用途:
1 .控制请求的超时和取消:context包允许在请求
超时或取消时通知Goroutine停止执行,以避免资
源泄漏和长时间阻塞.通过使用
context.WithTimeout 或
context.WithCancel 创建一个带有超时或
取消功能的上下文,可以在超时或取消时终
止相关操作。
2 .传递请求范围的值:context包允许在请
求范围内传递值.这对于在请求处理流程中共
享上下文信息非常有用,通过使用
context.WithValue创建一个带有键值对
的上下文,可以将该值与上下文关联,并在
整个请求处理过程中访问这些值。
3 .多个 Goroutine 之间传递上下文信息:
context包允许在多个Goroutine之间传
递上下文信息。当启动一个Goroutine并
希望它能够访问请求的上下文时,可以将上
下文作为参数传递给 Goroutine 或使用
context.WithValue创建一个新的上下文。
4 .中止并发操作:当需要中止一组并发操作
时,可以使用context包实现。通过创建一
个带有取消功能的上下文,并将该上下文传
递给多个Goroutine,可以在需要时取消或
终止这些 Goroutine 的执行
13、channel 是否线程安全?锁用在什么地方?
Channel本身是线程安全的。在Go语言的并发
模型中,Channel是用来在不同的Goroutine
之间进行数据通信的,当一个Goroutine向
Channel发送数据时,直到另一个Goroutine接
收到这个数据之前,该Goroutine将会被阻塞。
这种机制保证了Channel的数据在Goroutine
之间传递时的安全性。在对buf中的数据进行入
队和出队操作时,为当前chnnel使用了互斥锁,
防止多个线程并发修改数据
14、go channel 的底层实现原理 (数据结构)
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe. Pointer
sendx uint
recvx uint
elemsize uint16
elemtype * _type
closed uint32
recvq waitq
sendq waitq
lock mutex
}
1. dataqsiz ( 缓冲区大小) 对于无缓冲的
channel, dataqsiz字段的值为0 , 表示没有
分配数据缓冲区, 数据的发送和接收是直接进
行的, 需要发送和接收Goroutine同时准备好,
否则会阻塞. 对于有缓冲的channel, dataqsiz
字段的值表示分配的数据缓冲区的大小。缓冲区
的大小决定了channel可以存储的元素数量。
当缓冲区完全填满时, 继续向channel发送数据
会阻塞发送Goroutine, 当缓冲区为空时, 尝试从
channel 接收数据会阻塞接收 Goroutine。
2 . buf字段的作用是存储channel的实际数据
缓冲区. 对于无缓冲的channel, buf字段为nil ,
表示没有分配缓冲区, 数据发送和接收操作是直
接阻塞的。对于有缓冲的channel,buf字段指
向分配的内存,用于存储元素值
3. recvq ( 接收队列) 是一个用于存储等待接收
数据的Goroutine的队列. 当一个Goroutine
尝试从空的channel接收数据时, 它会被放置在
recvq中等待其他Goroutine向channel发送
数据. 一旦有数据可用, 被阻塞的Goroutine就
会从recvq中被唤醒,并继续执行
4. sendq ( 发送队列) 是一个用于存储等待发送
数据的Goroutine的队列. 当一个Goroutine
尝试向一个已满的channel发送数据时, 它会被
放置在sendq中等待其他Goroutine从channel
接收数据. 一旦channel有足够的空间可以接收
数据, 被阻塞的Goroutine就会从sendq中被唤
醒, 并继续执行发送操作。
5. waitq ( 等待队列) 是一个用于存储等待在
channel上进行发送或接收操作的Goroutine
的队列. 当一Goroutine尝试向已满的channel
发送数据或从空的channel接收数据时, 它会被
放置在waitq中等待其他Goroutine执行相反
的操作, 从而使发送或接收操作能够进行. 一旦
有对应的发送或接收操作完成, 等待的
Goroutine就会从waitq中被唤醒,并继续执行
15、 nil、关闭的 channel 再进行读、写、关闭会怎么样
1 .nil channel:未初始化channel,未经make
2 .closed channel:执行了closed的channel
3 .对nil channel的读写会永久block
4 .向closed channel写入会发生panic
5 .从closed channel读取仍然可以读取剩余
的数据,直到数据全部读取完成立即读出零值
6 .关闭nil channel,发生panic
16、 向 channel 发送数据和从 channel 读数据的流程是什么样的?
1 .向通道(channel)发送数据和从通道读取
数据是 Go 语言中实现并发通信的重要机制。
2 .向通道发送数据,创建通道: 使用 make
函数创建一个通道,指定通道的类型和容量
发送数据:使用< -操作符将数据发送到通道中。
3 .创建通道:使用make函数创建一个通道,指定
通道的类型和容量读取数据:使用 < - 操作符
从通道中读取数据。
4 .通道是阻塞的,如果没有接收者,发送操作
将被阻塞;如果没有发送者,接收操作也将
被阻塞。这种机制确保了 goroutine 之
间的同步和通信
17、map 使用注意的点,并发安全?
1 .map是引用类型,如果两个map同时指向一个
底层,那么一个map的变动会影响到另一个map。
2 .map的零值是nil,对nil map进行任何添
加元素的操作都会触发运行时错误( panic)
因此,使用前必须使用make先创建map,
m := make( map[ string] int) 。
3 .map的键可以是任何可以用== 或!= 操作符
比较的类型,如字符串,整数,浮点数,复数,
布尔等,但是slice,map,和function类
型不可以作为map的键
4 .map在使用过程中不保证遍历顺序,即:map
的遍历结果顺序可能会不一样
5 .map进行的所有操作,包括读取,写入,删除,
都是不安全的,也就是说,如果你在一个
goroutine中修改map,同时在另一个
goroutine中读取map会触发错误:
“concurrent map read and map write”
6 .Go语言的map不是并发安全的。并发情况下,
对map的读和写操作需要加锁,否则可能会因为
并发操作引起的竞态条件导致程序崩溃。为了在
并发环境下安全使用map,可以使用Go语言的
sync包中的sync.RWMutex读写锁,或者使用
sync.Map。
18、map 循环是有序的还是无序的?
for range循环map是无序的,因为map扩容?
重新哈希时,各键值项存储位置都可能会发生改
变,顺序自然也没法保证了,所以官方为了避免
依赖顺序,直接打乱处理。在进行循环遍历的
时候,生成了一个随机数作为遍历开始的位置,
可以for range循环map取出所有的key,
sort.Strings( keys) 排序所有的keys再
循环所有的keys.
19、 map 中删除一个 key,它的内存会释放么?
1 .如果删除的元素是值类型,如int,float,
bool,string以及数组和struct,map的内
存不会自动释放
2 .如果删除的元素是引用类型,如指针,slice,
map,chan等,map的内存会自动释放,但释放的
内存是子元素应用类型的内存占用
3 .将map设置为nil后,内存被回收
20、怎么处理对 map 进行并发访问?有没有其他方案?
1 .对整个map加上读写锁sync.RWMutex虽然
解决问题但是锁粒度大,影响性能
2.1 操作会导致整个map被锁住,导致性能降低
所以提出了分片思想,将一个map分成几个片,
按片加锁。第三方包实现:
github.com/orcaman/concurrent-map
3 .标准库中的sync.Map是专为append-only
场景设计的。sync.Map在读多写少性能比较好
否则并发性能很差。
21、 nil map 和空 map 有何不同?
Nil Map是指未初始化的Map或者是指被赋值
为nil 的Map 对象,
empty Map是指已经初始化,但是没有任何
元素的Map 对象。
22、map 的数据结构是什么?是怎么实现扩容?
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe. Pointer
oldbuckets unsafe. Pointer
nevacuate uintptr
extra * mapextra
}
1. 触发扩容有两种情况:
第一种: 负载因子超过了默认值, 就是正常元
素过多造成的扩容- 增量扩容
负载因子= 元素数量除桶数量, 默认为6.5
第二种: overflow的bucket数量过多, 就
是因为元素不断的进行增删造成溢出桶很多
元素很少,没有满足负载因子的默认值,但
是效率很低-- 等量扩容
2 . golang通过拉链发解决哈希冲突( 开放寻址法)
23、map 取一个 key,然后修改这个值,原 map 数据的值会不会变化
在Go语言中,map为引用类型,所以在修改map内
的值时,原数据也会随之变化,因此当你通过一个
key 取出一个值并对其进行修改时,将会影响到
原map 中该key 对应的值。
24、什么是 GMP?(必问)调度过程是什么样的?
1 .G - Goroutine也就是协程,是用户态
轻量级线程,一个goroutine大概需要2k
内存,在 64 位的机器上,可以轻松创建
几百万goroutine
2 .M - Machine Thread,也就是操作系
统线程,go runtime 最多允许创建1万个
操作系统线程,超过了就会抛出异常
3 .P - Processor逻辑处理器,默认数量
等于cpu核心数,通过环境变量GOMAXPROCS
改变其数量
4 .全局队列( GlobalQueue) 存放等待运行
的G,P还有个本地队列( 也称为LRQ) :存放等
待运行的G,但G的数量不超过256个。新建的
goroutine优先保存在P的本地队列中,如果
P的本地队列已满,则会保存到全局队列中
5 .P包含了运行G所需要的资源,M想要运行
goroutine必须先获取P然后从P的本地队列
获取Goroutine,P队列为空的时候,M也会
尝试从全局队列拿一批Goroutine放到P的
本地队列,或者从其他P的本地队列偷取一半
放到自己P的本地队列,M不断通过P获取
Goroutine并执行,不断重复下去。如果M
阻塞或者不够了,会创建新的M来支持。比如
所有的M都被阻塞住了,而P中还有很多的就
绪任务,调度器就会去寻找空闲的M。找不
到的话,就会去创建新的M,总而言之一个M
阻塞P就会去创建或者切换另一个M,
25、进程、线程、协程有什么区别?(必问)
1 .进程:是应用程序的启动实例,每个进程
都有独立的内存空间,不同的进程通过管道,
信号量,共享内存等方式来通信。
2 .线程:从属于进程,每个进程至少包含一
个线程,线程是CPU调度的基本单位,多线程
之间可以共享进程的资源,并通过共享内存
等方式来通信。
3 .协程:轻量级线程,与线程相比,协程不受
操作系统的调度,协程的调度器由用户程序
提供,协程调度器按照调度策略把协程调度
到线程中运行
26、抢占式调度是如何抢占的?
就像操作系统要负责线程的调度一样,Go的
runtime要负责goroutine的调度.现代
操作系统调度线程都是抢占式的,我们不能
依赖用户代码主动让出CPU,或者因为IO、
锁等待而让出,这样会造成调度的不公平。
基于经典的时间片算法,当线程的时间片用
完之后,会被时钟中断给打断,调度器会将
当前线程的执行上下文进行保存,然后恢
复下一个线程的上下文,分配新的时间片
令其开始执行。这种抢占对于线程本身是
无感知的,系统底层支持,不需要开发人
员特殊处理。
基于时间片的抢占式调度有个明显的优点,
能够避免CPU资源持续被少数线程占用,从
而使其他线程长时间处于饥饿状态。
goroutine的调度器也用到了时间片算法,
但是和操作系统的线程调度还是有些区别的,
因为整个Go程序都是运行在用户态的,所以
不能像操作系统那样利用时钟中断来打断运
行中的goroutine。也得益于完全在用户态
实现,goroutine的调度切换更加轻量。
27、怎么控制并发数?如何优雅的实现一个 goroutine 池
1 .有缓冲通道,三方库实现的协程池
2 . 为什么需要协程池,简单的协程池,
go-playground/pool, ants(推荐)
28、除了 mutex 以外还有那些方式安全读写共享变量?
1 .将共享变量的读写放到一个goroutine中,
其它goroutine通过channel进行读写操作。
2 .可以用个数为1的信号量( semaphore) 实现互斥
3 .通过 Mutex 锁实现
29、Go 如何实现原子操作?
1 .原子操作就是不可中断的操作,外界是看不到原
子操作的中间状态,要么看到原子操作已经完成,要
么看到原子操作已经结束。在某个值的原子操作执
行的过程中,CPU绝对不会再去执行其他针对该值
的操作,那么其他操作也是原子操作。
2 .Go语言的标准库代码包sync/atomic提供
了原子的读取( Load为前缀的函数或写入
( Store 为前缀的函数某个值
原子操作与互斥锁的区别
1 )、互斥锁是一种数据结构,用来让一个线程执
行程序的关键部分,完成互斥的多个操作。
2 )、原子操作是针对某个值的单个互斥操作。
30、Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?
1 .悲观锁:当要对数据库中的一条数据进行修改的
时候,为了避免同时被其他人修改,最好的办法就是
直接对该数据进行加锁以防止并发.这种借助数据库
锁机制,在修改数据之前先锁定,再修改的方式被称
之为悲观并发控制
2 .乐观锁是相对悲观锁而言的,乐观锁假设数据一般
情况不会造成冲突,所以在数据进行提交更新的时候
才会正式对数据的冲突与否进行检测,如果冲突,则
返回给用户异常信息,让用户决定如何去做。乐观锁
适用于读多写少的场景,这样可以提高程序的吞吐量
31、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 毫秒。
32、goroutine 的自旋占用资源如何解决
自旋锁是指当一个线程在获取锁的时候,如果锁
已经被其他线程获取,那么该线程将循环等待,
然后不断地判断是否能够被成功获取,直到获取
到锁才会退出循环。
自旋的条件如下:
1 )还没自旋超过 4 次,
2 )多核处理器,
3 )GOMAXPROCS > 1 ,
4 )p 上本地 goroutine 队列为空。
mutex 会让当前的 goroutine 去空转 CPU,
在空转完后再次调用 CAS 方法去尝试性的占有
锁资源,直到不满足自旋条件,则最终会加入到
等待队列里。
33、go三色标记法(必问)
1 .起初所有的对象都是白色的;
2 .从根对象出发扫描所有可达对象,标记为灰色,
放入待处理队列;
3 .从待处理队列中取出灰色对象,将其引用的对
象标记为灰色并放入待处理队列中,自身标记为
黑色;
4 .重复步骤(3),直到待处理队列为空,此时
白色对象即为不可达的“垃圾”,回收白色对象;
5 .屏障机制-STW垃圾回收过程中为了保证准确
性防止无止境的内存增长等问题而不可避免的需
要停止赋值器进一步操作对象图以完成垃圾回收。
STW时间越长,对用户代码造成的影响越大
34、知道哪些 sync 同步原语?各有什么作用?
1 .sync.Mutex
它允许在共享资源上互斥访问(不能同时访问)
2 .sync.RWMutex
是一个读写互斥锁
3 .sync.WaitGroup
sync.WaitGroup拥有一个内部计数器。当计
数器等于0时,则Wait( ) 方法会立即返回。否
则它将阻塞执行Wait( ) 方法的goroutine直
到计数器等于0时为止。要增加计数器,使用
Add( int) 方法。要减少它,使用Done( )
也可以传递负数给Add方法把计数器减少指定
大小,Done( ) 方法底层就是通过Add( -1)
实现的。
4 .sync.Map
是一个并发版本的Go语言的map
Store-添加元素。
Load-检索元素。
Delete-删除元素。
LoadOrStore检索或添加之前不存在的元素。
如果键之前在map中存在,则返回的布尔值为true。
使用Range遍历元素。
5 .sync.Pool
是一个并发池,负责安全地保存一组对象。
Get( ) 用来从并发池中取出元素。
Put将一个对象加入并发池。
6 .sync.Once
可确保一个函数仅执行一次
7 .sync.Cond
它用于发出信号(一对一)
或广播信号(一对多)到goroutine
35、知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?
1 ) 本该分配到栈上的变量,跑到了堆上,
这就导致了内存逃逸。
2 ) 栈是高地址到低地址,栈上的变量,
函数结束后变量会跟着回收掉,不会有
额外性能的开销。
3 ) 变量从栈逃逸到堆上,如果要回收掉,
需要进行gc,那么gc一定会带来额外的
性能开销。编程语言不断优化gc算法,
主要目的都是为了减少gc带来的额外性
能开销,变量逃逸会导致性能开销变大。
内存逃逸的情况如下:
1 )方法内返回局部变量指针。
2 )向 channel 发送指针数据。
3 )在闭包中引用包外的值。
4 )在 slice 或 map 中存储指针。
5 )切片(扩容后)长度太大。
6 )在 interface 类型上调用方法。
36、K8s 含有哪些重要组成部分
1 .Master节点:
API Server( API服务器) :提供了Kubernetes
API,用于与Kubernetes集群进行通信,
接受和处理各种命令。
Controller Manager( 控制器管理器) :负责运
行控制器( 如ReplicaSet Controller、
DeploymentController) ,监控集群状态
并根据所需状态进行调整。
Scheduler( 调度器) :负责将新创建的Pod调度
到集群中的Node上,考虑资源需求,约束条
件等因素。
2 .Node节点:
Kubelet:负责在Node上管理容器,包括启动、
停止、重启容器等。
Container Runtime( 容器运行时) :实际运行
容器的软件,如Docker,containerd等。
Kube Proxy:负责维护网络规则并将网络流量
转发到正确的容器服务。
3 .Etcd:
分布式键值存储,用于保存集群的配置信息,
状态信息等,被Master和Node节点共同使用。
4 .Pod:
最小部署单元,包含一个或多个容器.Pod中的容
器共享网络和存储,通常是紧密耦合的应用组件
5 .Service:
提供了一种抽象,用于定义一组Pod的访问方式.
Service可以保证一组Pod的稳定网络访问。
6 .Volume:
用于在Pod中持久化数据.Volume可以连接到
Node上的物理存储、网络存储等。
7 .Namespace:
用于将Kubernetes集群划分为多个虚拟集群,
以便在同一集群中运行多个不同的应用,环境等
8 .ConfigMap 和 Secret:
用于保存配置信息和敏感数据,可以在Pod中被
挂载为文件或环境变量。
9 .Ingress:
提供HTTP和HTTPS路由到服务的规则,允许外部
流量访问Kubernetes集群中的服务。
10 .StatefulSet:
用于部署有状态应用,确保Pod有唯一的标识和
稳定的网络标识标题
37、谈谈内存泄露,什么情况下内存会泄露?怎么定位排查内存泄漏问题?
go 中的内存泄漏一般都是goroutine泄漏,
就是 goroutine 没有被关闭,或者没有添
加超时控制,让 goroutine 一只处于阻塞
状态,不能被 GC
38、内存泄露有下面一些情况,以及解决方法
1 )如果goroutine在执行时被阻塞而无法
退出,就会导致goroutine的内存泄漏,一个
goroutine 的最低栈大小为 2KB,在高并
发的场景下,对内存的消耗也是非常恐怖的。
2 )互斥锁未释放或者造成死锁会造成内存泄漏
3 )time.Ticker是每隔指定的时间就会向
通道内写数据。作为循环触发器,必须调用
stop 方法才会停止,从而被 GC 掉,否则
会一直占用内存空间。
4 )字符串的截取引发临时性的内存泄漏
5 ) 切片截取引起子切片内存泄漏
6 ) 函数数组传参引发内存泄漏
一般通过pprof是Go的性能分析工具,在程序
运行过程中,可以记录程序的运行信息,可以
是CPU使用情况、内存使用情况、goroutine
运行情况等,当需要性能调优或者定位Bug时
候,这些记录的信息是相当重要
39、 MySQL 中的 B 树和 B+ 树区别
1 .存储方式: B 树和 B+ 树的存储方式不同。
在 B 树中,每个节点存储键和对应的值,而在
B+ 树中,只有叶子节点存储键和指向数据的指
针,非叶子节点只存储键。这意味着 B+ 树的
内部节点可以容纳更多的键,减少了树的高度,
从而减少了磁盘访问次数,提高了查询性能
2 .叶子节点结构:B树和B+树的叶子节点结构
也不同.在B树中,叶子节点包含了数据的实际
值,而在B+树中,叶子节点只包含键和指向数
据的指针.这使得B+树的叶子节点可以形成一
个有序链表,通过在链表上进行顺序遍历,可
以高效地获取范围内的数据。而 B 树则需要
在不同的层级进行跳跃,性能相对较低
4 .适用场景: B 树适用于需要随机访问的场
景,例如数据库索引。而B+树更适合范围查
询和顺序访问的场景,例如文件系统索引
40、http三次握手-建立Tcp连接和四次挥手-释放tcp链接
第一次握手(SYN= 1 ,seq= x):
客户端发送一个TCP的SYN标志位置1的包,
指明客户端打算连接的服务器的端口,以
及初始序号X,保存在包头的序列号
(Sequence Number)字段里。
第二次握手(SYN= 1 ,ACK= 1 ,seq= y,
ACKnum = x+1):服务器发回确认包
(ACK)应答。即SYN标志位和ACK标志位
均为1。服务器端选择自己的ISN序列号,
放在seq域里,同时将确认序号
(Acknowledgement Number)设置为
客户的ISN加1,即X+1。发送完毕后,服
务器端进入SYN_RCVD状态。
第三次握手(ACK= 1 ,ACKnum= y+1):
客户端再次发送确认包(ACK),SYN标志
位为0,ACK标志位为1,并且把服务器发来
ACK的序号字段+1,放在确定字段中发送给
对方,并且在数据段放写ISN的+1。
第一次挥手(FIN= 1 ,seq= x):
假设客户端想要关闭连接,客户端发送一个
FIN标志位置为1的包,表示自己已经没有数
据可以发送了,但是仍然可以接收数据。
发送完毕之后,客户端进入FIN_WAIT_1状态。
第二次挥手(ACK= 1 ,ACKnum= x+1):
服务器端确认客户端的FIN包,发送一个确认
包,表明自己接收到了客户端关闭连接的请求,
但还没有准备好关闭连接。发送完毕后,服务
器端进入CLOSE_WAIT状态,客户端接收到这
个确认包之后进入FIN_WAIT_2状态,等待服
务器端关闭连接。
第三次挥手(FIN= 1 ,seq= y):
服务器端准备好关闭连接时,向客户端发送结
束连接请求,FIN置为1。发送完毕后,服务
器端进入LAST_ACK状态,等待来自客户端的
最后一个ACK。
第四次挥手(ACK= 1 ,ACKnum= y+1):
客户端接收到来自服务器的端的关闭请求,发
送一个确认包,并进入TIME_WAIT状态,等
待可能出现的要重传的ACK包。
服务器端接收到这个确认包之后,关闭连接,
进入CLOSED状态。客户端等待了某个固定时间
(两个最大段生命周期,2MSL,2Maximum Segment Lifetime)之后,
没有收到服务器端的ACK,认为服务器端已经
正常关闭连接,于是自己也关闭连接,进入CLOSED状态。
41、 tcp 和 http 的区别
HTTP 是无状态的短连接,是应用层协议
定义的是传输数据的内容的规范
TCP是有状态的长连接 TCP是传输层协议
定义的是数据传输和连接方式的规范
42、Mysql 常见sql优化
1 、在where及order by涉及的列上建立索引。
2 、不要在where子句中进行的操作:
1 > 使用!= 或<> 操作符,
2 > 对字段进行null值判断
3 > 使用or来连接条件
4 > 使用参数
5 > 对字段进行表达式操作,函数操作
6 > 对“= ”左边进行函数、算术运算或其他表达式运算
7 > in和not in的使用,用between或exists代替in
导致索引失效,扫描全表
3 、like '%abc%' 导致索引失效
4 、不要使用 select * from t
6 、不要在有大量数据重复的列上建立索引
7 、一个表的索引数不要超过6个,索引提高
select 的效率,同时也降低了insert 及
update 的效率,因为可能会重建索引
8 、使用varchar代替char,因为变长字段存
储空间小,可以节省存储空间,其次对于查询
来说,在一个相对较小的字段内搜索效率
显然要高些。
9 、在使用索引字段作为条件时,如果该索引是
复合索引,那么必须使用到该索引中的第一个字
段作为条件时才能保证系统使用该索引,否则该
索引将不会被使用,并且应尽可能的让字段顺序
与索引顺序相一致。
10 、尽量使用数字型字段,若只含数值信息的
字段尽量不要设计为字符型
11 、尽量使用表变量来代替临时表。如果表变量
包含大量数据,请注意索引非常有限(只有主键索引)。
12 、避免频繁创建和删除临时表,以减少系统
表资源的消耗。
13 、临时表并不是不可使用,适当地使用它们
可以使某些例程更有效,例如,当需要重复引
用大型表或常用表中的某个数据集时。但是,
对于一次性事件,最好使用导出表。
14 、在新建临时表时,如果一次性插入数据量
很大,那么可以使用 select into 代替
create table,避免造成大量 log ,以
提高速度;如果数据量不大,为了缓和系统表
的资源,应先create table,然后insert。
15 、如果使用到了临时表,在存储过程的最后
务必将所有的临时表显式删除,先 truncate
table ,然后 drop table ,这样可以避免
系统表的较长时间锁定。
16 、尽量避免使用游标,因为游标的效率较差,
如果游标操作的数据超过1万行,那么就应该考
虑改写。
43、mysql 事务实现原理
通过日志的记录和重做、回滚日志的使用、锁的
管理和控制,并发控制机制的应用实现。这些机
制共同确保了事务的原子性、一致性、隔离性和
持久性,保证了数据库的数据完整性和并发操作
的正确性
1 . 日志(Log):
重做日志(Redo Log)**: 在事务进行修改操
作时,MySQL 将修改的内容记录到重做日志中,
以确保事务的修改可以持久化到磁盘。重做日志
是顺序写入的,因此具有较高的写入性能。
回滚日志(Undo Log)**: 在事务进行修改操
作之前,MySQL 将当前数据的副本记录到回滚日
志中。如果事务需要回滚,MySQL 可以使用回滚
日志将数据恢复到事务开始之前的状态。
2 . 锁(Lock):
行级锁(Row-Level Locking)**: MySQL
支持行级锁,允许在事务中对数据行进行加锁。
这样可以控制并发事务对同一数据的访问,确保
数据的一致性和隔离性。
锁粒度(Lock Granularity)**: MySQL
的锁粒度可以根据具体情况调整,包括表级锁、
页级锁和行级锁。较小的锁粒度可以提高并发
性能,但也会增加锁管理的开销。
3 . 隔离级别(Isolation Level)**:
MySQL 支持多个隔离级别,例如:
读未提交(Read Uncommitted)、
读已提交(Read Committed)、
可重复读(Repeatable Read)
串行化(Serializable)。
隔离级别定义了事务之间的隔离程度,
决定了事务读取数据的一致性和并发性。
4 . 并发控制(Concurrency Control)**:
MySQL 使用并发控制机制来处理多个并发
事务对数据库的访问。这包括锁的获取与释
放、冲突检测和解决,以及事务的调度和执行。
44、mysql中 explain工具的使用
type 字段可能的值
ALL < index < range ~ index_merge < ref < eq_ref < const < system
45、Go 的 select 底层数据结构和一些特性?
1 .go的 select 为golang提供了多路IO复用
机制,和其他IO复用一样,用于检测是否有读写事
件ready.linux的系统IO模型有select,poll,
epoll. select 结构组成主要是由 case 语
句和执行的函数组成
2 .select 的特性
1 )select操作至少要有一个 case 语句,出现
读写nil的channel该分支会忽略,在nil的
channel 上操作则会报错。
2 )select仅支持管道,而且是单协程操作。
3 )每个 case 语句仅能处理一个管道,要么读
要么写。
4 )多个 case 语句的执行顺序是随机的。
5 )存在default语句,select将不会阻塞,
但是存在 default 会影响性能。
46、单引号,双引号,反引号的区别?
1 .单引号:表示byte类型或rune类型,对应
uint8和int32类型,默认是rune类型。
2 .双引号:是字符串,实际上是字符数组。
3 .反引号:表示字符串字面量,但不支持任何转义
序列
47、redis 常见的数据类型
1 、string(字符串);
2 、hash(哈希);
3 、list(列表);
4 、set(集合);
5 、sort set (有序集合)
48、Innodb与Myisam引擎的区别
1 .InnoDB 支持事务MyISAM 不支持
2 .InnoDB 支持外键MyISAM 不支持
3 .InnoDB 是聚集索引,MyISAM是非聚集索引
聚簇索引的文件存放在主键索引的叶子节点上,
因此 InnoDB必须要有主键,通过主键索引效率
很高.但是辅助索引需要两次查询,先查询到主键,
然后再通过主键查询到数据.因此主键不应该过大
因为主键太大,其他索引也都会很大。而MyISAM
是非聚集索引,数据文件是分离的,索引保存的
是数据文件的指针。主键索引和辅助索引是独立的
4 .InnoDB 不保存表的具体行数,MyISAM保存了
整个表的行数
5 .InnoDB 最小的锁粒度是行锁,MyISAM是表锁
49、 什么时候触发行级锁,表级锁
1 .当执行DDL语句去修改表结构时,会使用表级锁。
2 .一般情况下都会默认使用行级锁,貌似是需要有索引匹配到才行