golang基础面试题总结

golang基础面试题总结

前言:由于正在准备之后的实习面试,故总结了一部分golang语言基础的问题,回答全为自己组织的语言,若有错各位大佬可及时指出,大家共同进步,谢谢。

1.go中怎样实现安全读写共享变量

要实现安全读写共享变量,也可以理解为在有一定竞争条件下保证并发读写的安全性,这就可以使用到go语言的一部分并发组件来达到这个目的了,首先是加锁,也就是加RWMutex互斥读写锁。其次是通过channel进行通信以实现共享内存空间来原子性的读写共享变量。

2.无缓冲的chan的发送和接收是否同步

是同步的,因为go语言中无缓冲的channel是阻塞的,只有channel中的数据被消费后,新数据才能写入。而从空channel中读取数据的goroutine则需要阻塞并等待一条数据的写入,这也就达到了发送和接收的同步。

3.Golang并发机制以及它所使用的CSP并发模型

由于golang中的并发机制的主要灵感则来自于CSP的原始论文和之后发展而来的并发原语,所以在此我先谈谈对CSP并发模型的理解。CSP全称是是通信顺序进程。所以CSP讲究的是“以通信的方式来共享内存”,用于描述两个独立的并发实体通过共享的 channel(管道)进行通信的并发模型。
golang中实现的channel就是被单独创建的用于在两个不同实体间进行消息传递。由于这种通信模式只注重于发送接收消息的管道而不是其中的实体,这也造就了实体间的解耦合。实体所发出的消息最终一定会被另一个实体所消费,也是类似于一种阻塞的消息队列。
而golang中的goroutine其实就是这种实际并发执行中实体的一个抽象,底层是使用协程进行并发。这里也就引出了协程的概念,golang里实现的协程在概念上我理解为一种在用户态运行实现的轻量级线程,它的一些区别于线程的特定也是它本身创建使用成本比较轻量级的原因。首先协程是运行在用户空间的,由golang自己的调度器实现调度,而这个调度器是在用户空间运行的,切换时不会trap进内核,这就直接避免了内核态和用户态的切换成本。而线程受内核调度,每次定时器中断造成的切换都需要保存寄存器、PC、栈等等信息,耗费的时间成本很高。其次goroutine在开启生命周期时创建的栈空间很小,一般只需要2K,并且大小会根据使用情况动态伸缩,最大可以扩充到1G,这就是goroutine轻量级的原因。
说回goroutine的特性,golang内部使用的是GMP调度模型,所对应的就是协程、线程、处理器。默认的单核情况下,创建的所有goroutine都先运行在一个线程中。每个线程都维护一个单独的处理器,任意时刻一个处理器中都只运行一个goroutine,其他的goroutine都在一个工作队列中等待。如果当前goroutine阻塞,就再创建一个线程并把处理器转移到新线程上运行,当原线程阻塞结束之后,会在其他线程上重新寻找一个空闲的处理器,否则就入到工作队列中。
所以综上,golang中的并发机制是以CSP作为基础理论根据,通过GMP调度模型也可以概括为事件循环、线程池、工作队列三个结构来调度相应的并发组件channel和goroutine以实现并发。

4.Golang中常用的并发模型

golang中常用的并发模型:
首先是for-select和channel的组合,有几种情况很适用,一种是需要迭代的向channel发送变量值。第二种是循环的等待停止,不管是有条件的有限循环还是无限循环都适用,比如值得一提的是用自带包中的ticker作时钟来实现一些定时的时间事件。
其次是使用sync包中的一些并发原语的组合类型,比如可以等待一组并发操作完成的waitGroup,调用Add(1)来表明goroutine的开始,defer Done()来保证当前goroutine向waitGroup表明已经安全退出。最后可以在main goroutine中调用wait()来阻塞等待waitGroup中所有的goroutine正确退出。还有就是用mutex锁来独占临界区嘛,比较简单。还有值得一提的就是cond类型,它可以阻塞的暂停挂起当前的goroutine等待或者发布一个事件。如果不使用cond原语来实现这种功能的话,最简单的方法就是使用一个无限循环,但是这会消耗整个cup的周期来等待条件的发生,而cond只会挂起当前goroutine,其他goroutine依然可以在内核级线程上继续运行。
最后是实现强大并发控制的context包,正常使用channel进行流动取消阻塞的并发操作是有限的,复杂生产环境下难以应对。context包主要实现的目的有两个,其一是提供取消调用分支的api,其二是提供传输请求范围数据的上下文数据包。

5.Go中nil slice和empty slice区别

nil slice所表示的是只有声明,但底层未进行数组实例化的切片,此时此切片值为nil。而empty slice则表示slice不为空,但底层数组是已经实例化,但空间没有值为空。

6. 协程和线程和进程的区别

进程可以概括为最小的系统资源分配调度的独立单位,拥有独立的虚拟内存空间,进行上下文切换时空间时间开销较大,需要保存栈、寄存器、页表、句柄等信息。以linux内核为例进程间通信方式主要分几种:
1.管道,通过pipe一个管道返回两个文件描述符一个用于写入一个用于读出,再fork一个子进程复制当前父进程的fd,就可以实现进程间通信。但是作为系统调用,fork开销大且数据缓存在内核,所以通信效率低下。
2.消息队列,通过保存在内核中的消息链表实现通信,发送时定义相应的数据类型且不会受进程影响而销毁,但数据大小也有限制,且用户态到内核态的数据拷贝也有一定开销。
3.共享内存,通过进程将自身虚拟页表映射到同一块内存上实现内存共享,数据发生任何更改都可及时响应,减少了去内核态拷贝的过程。但共享的内存作为临界区容易发生资源抢占覆盖等冲突问题,信号量则是保证进程间互斥同步的关键,作为一个整数计数器,可以通过PV两种不同操作加减信号量以表示当前临界区资源是否被占用。
再有就是属于例外情况下Linux信号和网络进程通信的套接字了。
而线程则可以概括为最小的独立执行单元,大多数线程作为内核态线程,不会被分配系统资源,只能在创建时分配进程中的一小部分内存来存储PC、寄存器、栈等等信息。线程间通信通过共享内存,上下文切换也需陷入到内核态,但相对于进程来说开销稍小。
协程的概念则是存在于用户态的轻量级线程,可由用户自定义分配,也有相应的内存来保存上下文信息,切换时在用户态进行无需陷入内核态,所以切换开销最小调度速度最快。

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

目前的golang版本使用的GC算法是三色标志法和混合写屏障,GC和goroutine可并发运行。但如果小对象过多,每次GC都会对栈上的可达对象进行扫描,且内存碎片多,cpu压力大。

8.Go中数据竞争问题怎么解决

可以使用sync包中的互斥锁或者CAS无锁并发,或者利用channel保证指令的原子性先行发生。

9.什么是channel,为什么它可以做到线程安全

golang中实现的channel就是被单独创建的用于在两个不同实体间进行消息传递的通信管道类型,可以理解为阻塞消息队列。由于channel底层使用了锁原语保证了读写的原子性,所以上层封装的读写接口也都是原子性的也就做到了对应的线程安全。

10. Golang垃圾回收算法

首先我想谈谈垃圾回收在整个程序运行期间的作用,最初垃圾回收的产生是因为内存管理的优化,传统系统及编程语言比如c的内存管理都是由开发者自主控制申请及施法,但一经不慎就有可能造成内存泄漏。最初是通过第三方静态扫描工具进行内存泄漏的检测,后来为了便利性,java\python\php等语言就将自动内存管理引入语言层面,内存释放由VM或Runtime管理。
垃圾回收算法最初使用引用计数,为每个对象维护相应的计数器,当新创建或新引用该对象时,计数器++,销毁更新时–,计数器为0后回收对象。但是由于频繁更新引用和循环引用问题导致性能不高。
还有标记-清除方法,标记当前程序所有的被引用对象,扫描完堆栈后再对所有未标记的对象进行垃圾回收,但由于每次扫描都会暂停STW,程序性能影响严重。
而golang中使用的则是标记-清除的优化方法三色标记法,并且和混和屏障结合保证不造成悬挂引用等问题。三色标记法主要是根据当前所有对象的引用情况分为白灰黑三种类型,最初所有栈对象都被初始化为白色,从当前程序开始遍历所有引用对象,标记为灰色,再遍历灰色对象列表将所有可达对象从当前白色列表移入灰色列表,将原灰色列表移入黑色列表。递归进行此过程直至灰色列表不含任何对象,最后白色列表中所剩下的则是空引用对象需要被回收。此时的标记过程是与程序并发进行的,所以新产生的引用对象有可能造成悬挂引用,因此go又引入了屏障机制防止误回收。插入写屏障保证当黑色对象新引用了对象时,防止错误清除该白色对象,将该对象变为灰色。删除写屏障保证当删除或解除白色对象时,将该对象标记为灰色,保证灰色到白色对象的路径不断。

11. GC的触发条件

当前golang中GC的触发条件分为主动和被动,主动触发时可以通过调用runtime包中相应的GC函数阻塞的等待当前GC循环完成。被动触发则是由系统进行监控,有定时事件和内存条件事件,go默认2分钟调用一次GC,或内存分配超过阈值时调用GC。

12. Go的GPM如何调度

golang内部使用的GMP调度模型,所对应的就是协程、线程、处理器。默认的单核情况下,创建的所有goroutine都先运行在一个线程中。每个线程都维护一个单独的处理器,任意时刻一个处理器中都只运行一个goroutine,其他的goroutine都在一个工作队列中等待。如果当前goroutine阻塞,就再创建一个线程并把处理器转移到新线程上运行,当原线程阻塞结束之后,会在其他线程上重新寻找一个空闲的处理器,否则就入到工作队列中。

13.并发编程概念是什么

提到并发就不得不先与并行做区分,并行的概念可以概括为多个事件在同一时刻发生,而并发则是指多个时间在同一时间间隔内交替发生。并行注重于在不同实体上的多个事件,而并发注重于在同一个实体上同时处理多个事件,尽管底层的多个事件间依旧是串行的。

14. Go语言的栈空间管理是怎么样的

目前版本下go选择的栈空间管理的方法是栈复制法,创建goroutine时先分配一个默认固定大小的栈内存空间。若栈溢出触发,会分配一个原栈两倍大小的新栈链接到原栈,并且将原栈内容全都复制到新栈中,若再需要扩展时会重用原栈空间。

15. 怎么查看Goroutine的数量

runtime包中有一个NumGoroutine函数来实时查看goroutine数量。

16. Go中的锁有哪些

go中实现的锁主要分为两种:互斥锁、读写锁。
互斥锁实现于sync包中的Mutex类型,暴露的方法接口包括lock、unlock,常常使用defer来保证锁的必然释放防止死锁。对已经上锁的对象再次申请上锁时就会受到阻塞直到该对象持有的锁释放,另外也要防止对未上锁的对象进行释放锁,这会导致发生panic并且此行为引发的panic无法用recover恢复。
读写锁也同样实现于sync包,主要是通过对读写操作进行单独上锁解锁。只要当前锁没有被用于写处理,就可以请求锁用于读处理,这时就被授予了访问权限,这也表明了同时读写的互斥性。而一旦当前锁已经用于写处理,则读写处理都不可再请求,所以综上读写锁的访问控制主要有几点,多个写操作互斥、读写互斥、多个读不互斥。另外读写锁也禁止对未上锁对象解锁等互斥锁不允许的会导致发生panic的行为。
最后是一些实现于sync/atomic包中的一系列特殊原子操作,CAS原子性比较、Load原子性加载等等。

17. 怎么限制Goroutine的数量

一个比较好的实现方式是自定义一个缓冲chan作协程池,并且使用waitGroup控制等待相应协程的创建释放。创建goroutine时发送信息到chan,释放时消费chan中的一个信息,并且在协程池中使用waitgroup等待goroutine执行完成。

18. Channel是同步的还是异步的

首先我认为不能简单的去判断channel是同步还是异步,channel可以通过缓冲与否以及多个复合channel的组合可以实现不同的并发模式,这里面也包括了同步、异步以及信息传递。
若要实现同步,可以实例化一个无缓冲channel来保证生产消费此channel的同步。因为消费空的无缓冲channel时会造成阻塞,向一个已经有数据的无缓冲channel生产也会造成阻塞。所以这就实现了同步需求。
若要实现异步,可以实例化有缓冲channel,生产者可以向channel中生产数据并且无需关心消费者的存在以及消费情况,消费者从channel中消费数据时也无需关心生产者的存在以及生产情况,只要不消费空channel或向满缓冲channel中生产数据,可以实现相应的异步需求。

19. Go的Struct能不能比较

由于go作为强类型语言,只有相同类型且具有相同属性的struct类型才可进行比较,且只能通过==比较。若struct类型不同,编译阶段则会出现类型不匹配的错误。

20. Go的defer原理

defer在go中的意义可以概括为延迟执行,若当前函数中有多个defer存在,执行顺序和栈结构存取相同,这是由于在runtime中定义了一个defer指针以链表实现的栈结构来将当前注册的defer操作链接到链表中。当函数执行return语句时,该操作会拆分为三部操作:先给返回值赋值,按顺序依次调用底层链表中的defer操作,最后在将返回值返回给调用函数。另外,defer操作中若有返回值,该返回值会被丢弃。
defer的使用场景常常在闭包和匿名函数中保证某种操作在函数出错或出现panic时也能正常执行,比如上锁后的解锁等等。

21. Go的Slice如何扩容

由于go中slice只是一个引用类型,只是为底层数组增加了一些可以动态扩容的接口,所以go中slice通过append填值时,若触发自动扩容,首先会检查当前容量是否小于定义的1024个元素,若小于则容量翻倍,若大于则增长因子为1.25翻1.25倍。容量发生翻倍后若小于底层数组容量则指针不变,若大于底层数组容量则malloc新数组并将原数组数据拷贝到新数组。

22. Go中select的作用及应用场景

go中的select机制可以理解为在语言层面实现的同时监听多个读写事件,若某个读写事件发生则执行相应的逻辑单元。在golang中的作用主要用于同时监听多个channel,其中的每个case是单独的事件,select开始时随机选择一个执行,若所有case未满足则执行default逻辑单元。

23. Go中的map如何实现顺序读取

由于go中的map定义的是单个bucket桶最多存8个键值对,所以负载因子超过6.5时或hash冲突导致的挂载桶超过2^15时会触发扩容,而扩容机制则是会造成当前map中键值对的搬迁,不管是增量还是等量扩容其实都已经造成了map中键值对的无序,所以在用for range循环map的时候底层已经主动给了一个随机种子。而如果要实现顺序读取,则需要先将数据读到一个大小合适的切片,再用sort包中的sort函数实现排序,从而实现顺序读取。

24.Go中的CAS原理

CAS是作为一种典型的无锁算法,通过不使用锁来保证不出现加锁带来的开销,go中实现的CAS则是在sync/atomic包中,通过CPU提供的原子性指令比较替换来实现非阻塞状态下的同步。
具体实现原理是通过将内存中的值与指定数据进行比较,若数值一样则替换为想要更改成的新值,若不一样则返回当前这个不一样的值。通过类似乐观锁的方式进行数据检查。总体来说,CAS就是靠cpu资源带来使用锁的开销。

25.Go值接收者和指针接收者的区别

最大的区别在于:如果方法的接收者是值类型,无论调用者是对象还是指针,修改的都是对象复制的副本,不影响调用者,而如果方法的接收者是指针类型,则调用者修改的接收指针指向的对象本身。所以本质上取决于是否要将接收者暴露给调用者。

26.Go的对象在内存中是怎样分配的

go中需要内存分配管理的都是堆内存,因为栈内存自顶向下无需回收。go在启main goroutine的时候会先申请一块虚拟地址空间内存,并分配成3个区域:spans\bitmap\arena,分别表示内存管理单元、标识arena区对象保存元数据、所谓的堆区。bitmap区域标识了哪些地址保存了对象,且用标志位表示对象是否包含指针、GC标记信息等等。

27. Go函数中为什么会发生内存泄露

首先是预期被GC标记清除的全局内存被某个变量附着且忽略了释放,则有可能发生内存泄漏。其次是goroutine泄漏,如果当前goroutine持续产生子goroutine且未及时结束,则会造成内存泄漏。

28. Go中new和make的区别

go中new关键字旨在申请一个该类型大小的内存空间并初始化零值再返回内存空间的指针。而make旨在用于内存分配,包括slice\map\channel等有底层具体数据结构的类型的内存创建并初始化,并且返回的是具体类型。

29. Go中的锁如何实现

go的锁类型主要包含互斥锁和读写锁,互斥锁底层主要由当前锁状态枚举值和信号量,锁状态一个包含8byte位,从第到高依次表示当前锁定状态、是否被唤醒、是否进入饥饿状态、正在当前互斥锁上等待的goroutine个数。
当互斥锁处于正常模式时,正在等待锁的队列按照队列顺序来获取锁,但若是刚被唤起的goroutine和新创建的goroutine处于竞争时会获取不到锁,所以一旦goroutine超过1ms未获取到锁,则会由正常模式切换到饥饿模式,防止goroutine饿死。
当处于饥饿模式时,互斥锁直接交给当前队列最前方的goroutine,新goroutine不能自旋不能获取,只能在队尾排队等待。如果有goroutine获得了锁或等待时间少于1ms则切换回正常模式,饥饿模式主要是防止goroutine陷入等待而造成高延迟。
互斥锁加锁时先查询锁定状态位是否为0,是0则通过原子操作置1,不是0则自旋等待锁释放。若等待时间过长则进入饥饿模式逻辑,否则持续处于正常模式以提供高性能。
互斥锁解锁时先查询当前锁是否加锁,若没加锁则抛出异常,程序panic。当互斥锁处于饥饿模式,直接将锁所有权交给下个等待者。当处于正常模式,若没有goroutine正在等待锁则直接返回,若有则唤醒相应的goroutine。
读写锁主要依靠复用互斥锁,并加入了写等待读、读等待写的阻塞数量以保证读写、写读、写写操作的互斥。

30. Go中的channel的实现

go中channel的实现主要是来源于CSP并发模型,通过通信来实现共享内存。goroutine和channel则分别是CSP并发模型中的实体及传递信息的媒介,channel读写操作都是遵循FIFO队列设计。有缓冲channel底层使用了一个数组实现的环形队列保存缓冲区数据,并有相应的读写阻塞列表作为等待队列。go中使用make来初始化channel,执行时编译器重写到底层的makechan函数,函数根据相应的数据类型及缓冲区大小初始化底层结构体及缓冲区队列。

31. Go中的map的实现

go中的map作为KV对集合,底层使用hashtable作存储,并用链表实现冲突解决,出现冲突时以bmap作为最小粒子挂载,一个bmap可以存放8个KV键值对。map底层结构以hmap,也就是桶来实现,通过hash决定落入哪个桶,再通过hash值高八位决定桶内位置。
读取时依次遍历正常桶和溢出桶数据,通过先比较hash高八位和tophash,再比较值以加速数据读写。
写入时遍历比较桶中tophash与键hash确定桶地址,如果桶满则调用函数创建新桶或使用缓冲区中的桶来保存数据。
当hash性能低下达到了扩容临界点时,其一是装载因子超过6.5,触发超量扩容,创建新桶rehash以保证装载因子的正常。其二是溢出桶过量超过2^15,触发等量扩容,此时数据量没有超过阈值但溢出桶过多容易造成内存泄漏,所以实现了类似“桶压缩”。

参考文献

1.Katherine Cox-Buday: Concurrency in go. ISBN: 978-7-5198-2494-5.
2.Brian W. Kernighan / Alan Donovan: The Go Programming Language. ISBN: 978-0-1341-9044-0.

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

筱2402

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值