问:数组和切片的相同点和区别
相同点:
- 只能存储一组相同类型的数据结构
- 都是通过下标来访问,并且有容量长度,长度通过len获取,容量通过cap获取
区别:
- 数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩充
- 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组内存地址也就随之改变。
问:for range 的时候它的地址会发生变化么?
答:在for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for循环里面如果开协程,不要直接把a或者b的地址传给协程。
问:Go多返回值怎么实现的
答:Go传参和返回值是通过FP offset实现,并且存储在调用函数的栈帧中。FP栈底寄存器,指向一个函数栈的顶部;PC程序计数器,指向下一条执行指令;SB指向静态数据的基指针,全局符号;SP 栈顶寄存器。
问:map相关的一些问题
问:map 使用注意的点,并发安全? 并发不安全,如果出现两个以上的协程写同一个map会报错,使用读写读写锁解决。 问:map 循环是有序的还是无序的? 无序的 问:map 中删除一个 key,它的内存会释放么? 通过delete删除map的key,执行gc不会,内存没有被释放,如果通过map=nil,内存才会释放 问:怎么处理对 map 进行并发访问? 通过加读写锁RWMutex,也可以使用sync.Map 问:nil map 和空 map 有何不同? nil map是未初始化的map,空map是长度为空
问:哪些方式安全读写共享变量
答:
- 将共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。
- 可以用个数为 1 的信号量(semaphore)实现互斥
- 通过Mutex 锁实现
问:Go 如何实现原子操作
答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。 Go语言的标准库代码包sync/atomic提供了原子的读取(Load为前缀的函数)或写入(Store为前缀的函数)某个值 原子操作与互斥锁的区别
- 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
- 原子操作是针对某个值的单个互斥操作。
问:Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?
答:Mutex是悲观锁 悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。 乐观锁:乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
问:Mutex 有几种模式?
sync.Mutex 有两种模式,正常模式和饥饿模式。 正常模式:等待的goroutines按照FIFO(先进先出)顺序排队,但是goroutine被唤醒之后并不能立即得到mutex锁,它需要与新到达的goroutine争夺mutex锁。因为新到达的goroutine已经在CPU上运行了,所以被唤醒的goroutine很大概率是争夺mutex锁是失败 的。出现这样的情况时候,被唤醒goroutine需要排队在队列的前面。如果被唤醒的goroutine有超过1ms没有获取到mutex锁,那么它就会变为饥饿模式。在饥饿模式中,mutex锁直接从解锁的goroutine交给队列前面的goroutine。新达到的goroutine也不会去争夺mutex锁(即使没有锁,也不能去自旋),而是到等待队列尾部排队。正常模式有更好的性能,因为goroutine可以连续多次获得mutex锁。 饥饿模式:锁的所有权将从unlock的gorutine直接交给交给等待队列中的第一个。新来的goroutine将不会尝试去获得锁,即使锁看起来是unlock状态,也不会去尝试自旋操作,而是放在等待队列的尾部。如果有一个等待的goroutine获取到mutex锁了,如果它满足下条件中的任意一个, mutex将会切换回去正常模式:是等待队列中的最后一个goroutine和它的等待时间不超过1ms。饥饿模式能阻止尾部延迟的现象,对于预防队列尾部goroutine一致无法获取mutex锁的问题。
问:goroutine 的自旋占用资源如何解决?
答:自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。 自旋的条件如下:
- 还没自旋超过 4 次,
- 多核处理器,
- GOMAXPROCS > 1,
- p 上本地 goroutine 队列为空。
mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。
问:谈谈内存泄漏,什么情况下内存会泄漏?怎么定位排查内存泄漏问题?
答:go中的内存泄漏一般都是goroutine泄漏,就是goroutine没有被关闭,或者没有添加超时控制,让goroutine一只处于阻塞状态,不能被GC。
内存泄露有下面一些情况
- 如果goroutine在执行时被阻塞而无法退出,就会导致goroutine的内存泄漏,一个goroutine的最低栈大小为2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。
- 互斥锁未释放或者造成死锁会造成内存泄漏
- time.Ticker是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用stop方法才会停止,从而被GC掉,否则会一直占用内存空间。
- 字符串的截取引发临时性的内存泄漏
func main() {
var str0 = "12345678901234567890"
str1 := str0[:10]
}
- 切片截取引起子切片内存泄漏
func main() {
var s0 = []int{0,1,2,3,4,5,6,7,8,9}
s1 := s0[:3]
}
- 函数数组传参引发内存泄漏【如果我们在函数传参的时候用到了数组传参,且这个数组够大(我们假设数组大小为100万,64位机上消耗的内存约为800w字节,即8MB内存),或者该函数短时间内被调用N次,那么可想而知,会消耗大量内存,对性能产生极大的影响,如果短时间内分配大量内存,而又来不及GC,那么就会产生临时性的内存泄漏,对于高并发场景相当可怕。】
排查方式:一般通过pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。
问:请简述 Go 是如何分配内存的?
Go程序启动的时候申请一大块内存,并且划分spans,bitmap,areana区域;arena区域按照页划分成一个个小块,span管理一个或者多个页,mcentral管理多个span供现场申请使用;mcache作为线程私有资源,来源于mcentral。
问:Channel 分配在栈上还是堆上?
Channel被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以golang直接将其分配在堆上。
问:介绍一下大对象小对象,为什么小对象多了会造成 gc 压力?
小于等于32k的对象就是小对象,其它都是大对象。一般小对象通过 mspan 分配内存;大对象则直接由 mheap 分配内存。通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。
参考文献:
书籍《go专家编程》