1. Go有哪些关键字
2. Go有哪些数据类型
3. Go内置函数make和new的区别?
new和make函数主要是用来给对象分配内存的
使用场景区别:
1. make只能用于slice、map、chan,它们可以只声明不初始化
2. new可以初始化任意类型的数据,并且置零
返回值区别:
1. make函数返回的是slice、map、chan类型本身,这三种类型是引用类型,就没必要返回他们的指针
2. new返回该类型内存地址的指针
4. Go类型
基本类型:数值、浮点、字符串、布尔、数组 及 错误 类型
引用类型:切片(slice)、字典(map)、接口(interface)、函数(func) 以及 通道(chan)
结构类型(也是值传递):struct
5. Go import三种方式
除了直接import,常见的import有三种特殊形式,分别是前面加下划线(_),加点(.),加别名。
加下划线:有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import _ 引用该包。即:使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
加点(.):import和引用的包名之间加点(.)操作的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名。
别名:别名操作顾名思义可以把包命名成另一个用起来容易记忆的名字。
6. go子协程panic主协程会结束吗
会,如果一个 Go 协程中出现 panic,那么这个协程就会终止。如果主协程里有代码来捕获这个 panic,那么它可以在 panic 发生后继续执行;但是如果主协程没有做任何处理,那么程序也会终止。
7、Go struct能不能比较
struct中含有不能被比较的字段类型就不能被比较,如果所有字段类型都支持比较,那么就可以比较。
不可比较类型:slice、map、函数类型,因为他们是引用
其他类型都可以比较
结构体之间只能比较他们是否相等,不能比较他们的大小,且只有所有属性都相等且属性顺序都一致的结构体才可比较。
8、Go init函数执行顺序
变量初始化->init()->main()
init函数的主要作用:
1. 初始化不能采用初始化表达式初始化的变量
2. 程序运行前的注册
3. 实现sync.Once功能(单例+读锁)sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似,但有区别。
特点:不能被其他函数调用、同一个包init执行顺序没有明确定义,不要依赖这个顺序
9.
10. Go interface{}
11. byte和rune有什么区别
run是一个字符包括中文字符,byte是一个字节
12. Go的执行顺序
13. 数组和切片的区别
相同点:
1. 只能存储一组相同类型的数据结构
2. 都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取
区别:
1. 数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩充
2. 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组内存地址也就随之改变。
14. Go mod主要解决了什么问题
15. Go sum里面是什么内容
1. go sum的意义
2. go sum的内容
16. 进程线程协程的区别
17. 相比较于其他语言, Go 有什么优势或者特点?
1. Go 允许跨平台编译,编译出来的是二进制的可执行文件,直接部署在对应系统上即可运行。
2. Go 在语言层次上天生支持高并发,通过 goroutine 和 channel 实现。channel 的理论依据是 CSP 并发模型, 即所谓的通过通信来共享内存;Go 在 runtime 运行时里实现了属于自己的调度机制:GMP,降低了内核态和用户态的切换成本。
3. Go 的代码风格是强制性的统一,如果没有按照规定来,会编译不通过。
18. Golang 里的 GMP 模型?
GMP 模型是 golang 自己的一个调度模型,它抽象出了下面三个结构:
G:也就是协程 goroutine,由 Go runtime 管理。我们可以认为它是用户级别的线程。
P:processor 处理器。每当有 goroutine 要创建时,会被添加到 P 上的 goroutine 本地队列上,如果 P 的本地队列已满,则会维护到全局队列里。
M:系统线程。在 M 上有调度函数,它是真正的调度执行者,M 需要跟 P 绑定,并且会让 P 按下面的原则挑出个 goroutine 来执行:
优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取,如果全局队列也没有,会从其他的 P 上偷取 goroutine。
19. goroutine 的协程有什么特点,和线程相比?
goroutine 非常的轻量,初始分配只有 2KB,当栈空间不够用时,会自动扩容。同时,自身存储了执行 stack 信息,用于在调度时能恢复上下文信息。
而线程比较重,一般初始大小有几 MB(不同系统分配不同),线程是由操作系统调度,是操作系统的调度基本单位。而 golang 实现了自己的调度机制,goroutine 是它的调度基本单位。
20. Go 的垃圾回收机制?
Go 采用的是三色标记法,将内存里的对象分为了三种:
白色对象:未被使用的对象;
灰色对象:当前对象有引用对象,但是还没有对引用对象继续扫描过;
黑色对象,对上面提到的灰色对象的引用对象已经全部扫描过了,下次不用再扫描它了。
当垃圾回收开始时,Go 会把根对象标记为灰色,其他对象标记为白色,然后从根对象遍历搜索,按照上面的定义去不断的对灰色对象进行扫描标记。当没有灰色对象时,表示所有对象已扫描过,然后就可以开始清除白色对象了。
21. go 的内存分配是怎么样的?
Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。具体的分配逻辑如下:
当要分配大于 32K 的对象时,从 mheap 分配。
当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。
22. channel 的内部实现是怎么样的?
channel 内部维护了两个 goroutine 队列,一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。
每当对 channel 的读写操作超过了可缓冲的 goroutine 数量,那么当前的 goroutine 就会被挂到对应的队列上,直到有其他 goroutine 执行了与之相反的读写操作,将它重新唤起。
23. 对已经关闭的 channel 进行读写,会怎么样?
当 channel 被关闭后,如果继续往里面写数据,程序会直接panic退出。如果是读取关闭后的 channel,不会产生 pannic,还可以读到数据。但关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。
24. map 为什么是不安全的?
map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。
25. map 的 key 为什么得是可比较类型的?
map 的 key、value 是存在 buckets 数组里的,每个 bucket 又可以容纳 8 个 key 和 8 个 value。当要插入一个新的 key - value 时,会对 key 进行 hash 运算得到一个 hash 值,然后根据 hash 值 的低几位(取几位取决于桶的数量,比如一开始桶的数量是 5,则取低 5 位)来决定命中哪个 bucket。
在命中某个 bucket 后,又会根据 hash 值的高 8 位来决定是 8 个 key 里的哪个位置。如果不巧,发生了 hash 冲突,即该位置上已经有其他 key存在了,则会去其他空位置寻找插入。如果全都满了,则使用 overflow 指针指向一个新的 bucket,重复刚刚的寻找步骤。
从上面的流程可以看出,在判断 hash 冲突,即该位置是否已有其他 key时,肯定是要进行比较的,所以 key 必须得是可比较类型的。像 slice、map、function 就不能作为 key。
26. mutex 的正常模式、饥饿模式、自旋?
正常模式
当 mutex 调用 Unlock() 方法释放锁资源时,如果发现有正在阻塞并等待唤起的 Goroutine 队列时,则会将队头的 Goroutine 唤起。队头的 goroutine 被唤起后,会采用 CAS 这种乐观锁的方式去修改占有标识位,如果修改成功,则表示占有锁资源成功了,当前占有成功的 goroutine 就可以继续往下执行了。
饥饿模式
由于上面的 Goroutine 唤起后并不是直接的占用资源,而是使用 CAS 方法去尝试性占有锁资源。如果此时有新来的 Goroutine,那么它也会调用 CAS 方法去尝试性的占有资源。对于 Go 的并发调度机制来讲,会比较偏向于 CPU 占有时间较短的 Goroutine 先运行,即新来的 Goroutine 比较容易占有资源,而队头的 Goroutine 一直占用不到,导致饿死。
针对这种情况,Go 采用了饥饿模式。即通过判断队头 Goroutine 在超过一定时间后还是得不到资源时,会在 Unlock 释放锁资源时,直接将锁资源交给队头 Goroutine,并且将当前状态改为饥饿模式。
后面如果有新来的 Goroutine 发现是饥饿模式时, 则会直接添加到等待队列的队尾。
自旋
如果 Goroutine 占用锁资源的时间比较短,那么每次释放资源后,都调用信号量来唤起正在阻塞等候的 goroutine,将会很浪费资源。
因此在符合一定条件后,mutex 会让等候的 Goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终才加入到等待队列里。
自旋的条件如下:
1. 还没自旋超过 4 次,
2. 多核处理器,
3. GOMAXPROCS > 1,(线程数、逻辑CPU数量)
4. p 上本地 goroutine 队列为空。
27. Go 的逃逸行为是指?
在传统的编程语言里,会根据程序员指定的方式来决定变量内存分配是在栈还是堆上,比如声明的变量是值类型,则会分配到栈上,或者 new 一个对象则会分配到堆上。
在 Go 里变量的内存分配方式则是由编译器来决定的。如果变量在作用域(比如函数范围)之外,还会被引用的话,那么称之为发生了逃逸行为,此时将会把对象放到堆上,即使声明为值类型;如果没有发生逃逸行为的话,则会被分配到栈上,即使 new 了一个对象。
28. waitgroup 原理
waitgroup 内部维护了一个计数器,当调用wg.Add(1)方法时,就会增加对应的数量;当调用wg.Done()时,计数器就会减一。直到计数器的数量减到 0 时,就会调用 runtime_Semrelease 唤起之前因为wg.Wait()而阻塞住的 goroutine。
29. sync.Once 原理
内部维护了一个标识位,当它 == 0 时表示还没执行过函数,执行对应函数,然后加锁修改标识位为1。后续再执行时发现标识位 != 0,则不会再执行后续动作了。
30. 定时器原理
一开始,timer 会被分配到一个全局的 timersBucket 时间桶。每当有 timer 被创建出来时,就会被分配到对应的时间桶里了。
为了不让所有的 timer 都集中到一个时间桶里,Go 会创建 64 个这样的时间桶,然后根据 当前 timer 所在的 Goroutine 的 P 的 id 去哈希到某个桶上:
接着 timersBucket 时间桶将会对这些 timer 进行一个最小堆的维护,每次会挑选出时间最快要达到的 timer。如果挑选出来的 timer 时间还没到,那就会进行 sleep 休眠;如果 timer 的时间到了,则执行 timer 上的函数,并且往 timer 的 channel 字段发送数据,以此来通知 timer 所在的 goroutine。
31. gorouinte 泄漏有哪些场景
gorouinte 里有关于 channel 的操作,如果没有正确处理 channel 的读取,会导致 channel 一直阻塞住, goroutine 不能正常结束
32. Slice 的扩容机制
如果 Slice 要扩容的容量大于 2 倍当前的容量,则直接按想要扩容的容量来 new 一个新的 Slice,否则继续判断当前的长度 len,如果 len 小于 1024,则直接按 2 倍容量来扩容,否则一直循环新增 1/4,直到大于想要扩容的容量。除此之外,还会根据 slice 的类型做一些内存对齐的调整,以确定最终要扩容的容量大小。 主要代码如下:
slice 入参注意点
如果 slice 作为函数的入参,通常希望对 slice 的操作可以影响到底层数据,但是如果在函数内部 append 数据超过了 cap,导致重新分配底层数组,这时修改的 slice 将不再是原来入参的那个 slice 了。因此通常不建议在函数内部对 slice 有 append 操作,若有需要则显式的 return 这个 slice。
33. defer、panic、recover 三者的用法
defer 函数调用的顺序是后进先出,当产生 panic 的时候,会先执行 panic 前面的 defer 函数后才真的抛出异常。一般的,recover 会在 defer 函数里执行并捕获异常,防止程序崩溃。
34. map 相关的一些问题
问:map 使用注意的点,并发安全?并发不安全,如果出现两个以上的协程写同一个 map 会报错,使用读写读写锁解决。
问:map 循环是有序的还是无序的?无序的
问:map 中删除一个 key,它的内存会释放么?通过 delete 删除 map 的 key,执行 gc 不会,内存没有被释放,如果通过 map=nil,内存才会释放
问:怎么处理对 map 进行并发访问?通过加读写锁 RWMutex,也可以使用 sync.Map
问:nil map 和空 map 有何不同?nil map 是未初始化的 map,空 map 是长度为空
35. Go 如何实现原子操作
答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU 绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。
Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值
原子操作与互斥锁的区别
1. 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
2. 原子操作是针对某个值的单个互斥操作。
36. Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?
答:Mutex 是悲观锁
悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制
乐观锁:乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
37. 内存泄露
答:go 中的内存泄漏一般都是 goroutine 泄漏,就是 goroutine 没有被关闭,或者没有添加超时控制,让 goroutine 一只处于阻塞状态,不能被 GC。
内存泄露有下面一些情况
1.如果 goroutine 在执行时被阻塞而无法退出,就会导致 goroutine 的内存泄漏,一个 goroutine 的最低栈大小为 2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。
2.互斥锁未释放或者造成死锁会造成内存泄漏
3.time.Ticker 是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用 stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间。
4.字符串的截取引发临时性的内存泄漏
func main() { var str0 = "12345678901234567890" str1 := str0[:10]}
5.切片截取引起子切片内存泄漏
func main() { var s0 = []int{0,1,2,3,4,5,6,7,8,9} s1 := s0[:3]}
6.函数数组传参引发内存泄漏【如果我们在函数传参的时候用到了数组传参,且这个数组够大(我们假设数组大小为 100 万,64 位机上消耗的内存约为 800w 字节,即 8MB 内存),或者该函数短时间内被调用 N 次,那么可想而知,会消耗大量内存,对性能产生极大的影响,如果短时间内分配大量内存,而又来不及 GC,那么就会产生临时性的内存泄漏,对于高并发场景相当可怕。】
排查方式:一般通过 pprof 是 Go 的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是 CPU 使用情况、内存使用情况、goroutine 运行情况等,当需要性能调优或者定位 Bug 时候,这些记录的信息是相当重要。
38. Channel 分配在栈上还是堆上?
Channel 被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以 golang 直接将其分配在堆上。
39. 介绍一下大对象小对象,为什么小对象多了会造成 gc 压力?
小于等于 32k 的对象就是小对象,其它都是大对象。一般小对象通过 mspan 分配内存;大对象则直接由 mheap 分配内存。通常小对象过多会导致 GC 三色法消耗过多的 CPU。优化思路是,减少对象分配。
40. 逃逸分析
go在分配内存时,编译器通过逃逸分析技术去选择堆或者栈,逃逸分析的基本思想如下:检查变量的生命周期是否是完全可知的,如果通过检查,则在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配。
逃逸分析原则:
Go语言虽然没有明确说明逃逸分析原则,但是有以下几点准则,是可以参考的。
不同于JAVA JVM的运行时逃逸分析,Go的逃逸分析是在编译期完成的:编译期无法确定的参数类型必定放到堆中;
如果变量在函数外部存在引用,则必定放在堆中;
如果变量占用内存较大时,则优先放到堆中;
如果变量在函数外部没有引用,则优先放到栈中;
变量大小不确定时
41. 解释一下并发编程中的自旋状态?
自旋状态是并发编程中的一种状态,指的是线程或进程在等待某个条件满足时,不会进入休眠或阻塞状态,而是通过不断地检查条件是否满足来进行忙等待。
在自旋状态下,线程会反复执行一个忙等待的循环,直到条件满足或达到一定的等待时间。这种方式可以减少线程切换的开销,提高并发性能。然而,自旋状态也可能导致 CPU 资源的浪费,因为线程会持续占用 CPU 时间片,即使条件尚未满足。
自旋状态通常用于以下情况:
在多处理器系统中,等待某个共享资源的释放,以避免线程切换的开销。
在短暂的等待时间内,期望条件能够快速满足,从而避免进入阻塞状态的开销。
需要注意的是,自旋状态的使用应该谨慎,并且需要根据具体的场景和条件进行评估。如果自旋时间过长或条件不太可能很快满足,那么使用自旋状态可能会浪费大量的 CPU 资源。在这种情况下,更适合使用阻塞或休眠等待的方式。
总之,自旋状态是一种在等待条件满足时不进入休眠或阻塞状态的并发编程技术。它可以减少线程切换的开销,但需要权衡 CPU 资源的使用和等待时间的长短。
42. Go 语言中的 goroutine 是什么?
goroutine 是 Go 语言中轻量级的并发执行单元,可以同时执行多个 goroutine,而不需要显式地管理线程的生命周期。goroutine 由 Go 运行时(runtime)进行调度,可以在并发编程中实现并行执行。
43. Go 语言中的通道(channel)是什么?
通道是用于在 goroutine 之间进行通信和同步的机制。通道提供了一种安全的、阻塞的方式来发送和接收数据。通过通道,可以实现多个 goroutine 之间的数据传递和同步。
44. Go 语言中的 select 语句是什么?
select 语句是 Go 语言中用于处理通道操作的一种机制。它可以同时监听多个通道的读写操作,并在其中任意一个通道就绪时执行相应的操作。
45. Go 语言中的 runtime 包是用来做什么的?
runtime 包是 Go 语言的运行时系统,提供了与底层系统交互和控制的功能。它包含了与内存管理、垃圾回收、协程调度等相关的函数和变量。