Golang后端面试汇总-001

流雨声 · · 765 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

基础面试

  • go的调度
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

Go的调度器使用了三种结构:M,P,S

M代表内核线程,类似于标准的POSIX线程,M代表machine。
G代表goroutine,它拥有自己的栈,程序计数器(instruction counter)和一些关于goroutine调度的信息(如正在阻塞的channel)。
P代表processor,表示调度的上下文。可以把它看作是一个局部的调度器,让Go代码跑在一个单独的线程上。这是让Go从一个N:1调度器映射到一个M:N调度器的关键。

参考地址: 
a. https://www.cnblogs.com/Real-m/p/13885918.html
b. https://studygolang.com/articles/20991
  • 为什么在内核的线程调度器之外Go还需要一个自己的调度器?
1. POSIX线程API是对已有的UNIX进程模型的逻辑扩展,因此线程和进程在很多方面都类似。例如,线程有自己的信号掩码,CPU affinity(进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性
),cgroups。但是有很多特性对于Go程序来说都是累赘。 
2. 另外一个问题是基于Go语言模型,OS的调度决定并不一定合理。例如,Go的垃圾回收需要内存处于一致性的状态,这需要所有运行的线程都停止。垃圾回收的时间点是不确定的,如果仅由OS来调度,将会由大量的线程停止工作。
单独开发一个Go的调度器能让我们知道什么时候内存处于一致性的状态。也就是说,当开始垃圾回收时,运行时只需要为当时正在CPU核上运行的那个线程等待即可,而不是等待所有的线程
  • go struct能不能比较
struct因为是强类型语言,所以不同类型的结构不能作比较,但是同一类型的实例值是可以比较的,实例不可以比较,因为是指针类型
  • go defer(for defer)
先进后出,后进先出
1,defer是什么?
defer 就像它的字面意思一样,就是延迟执行的意思,但是需要注意的是 defer 只能作用于函数,像变量赋值defer i = 10这种编译是会报错的。

2,defer函数的执行顺序
被 defer 的函数会放入一个栈中,所以是先进后出的执行顺序,而被 defer 的函数在 return 之后执行。

3,清理释放资源
当打开一个文件时,用完之后我们需要 close 这个文件,否则会导致文件描述符泄露;

4, 执行recover
被 defer 的函数在 return 之后执行,这个时机点正好可以捕获函数抛出的 panic,因而defer 的另一个重要用途就是执行 recover ,而 recover 也只有在 defer 中才会起作用。

Go 语言中的闭包就是在函数内引用函数体之外的数据,这样就会产生一种结果,虽然数据定义是在函数外,但是在函数内部操作数据也会对数据产生影响。

参考链接: https://blog.csdn.net/HYZX_9987/article/details/103727233
  • select可以用于什么
用于gorotine的完美退出
golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作

参考链接: https://www.cnblogs.com/gwyy/p/13629999.html
  • context包的用途
context包的用途Context通常被译作上下文,它是一个比较抽象的概念,其本质,是【上下上下】存在上下层的传递,上会把内容传递给下。在Go语言中,程序单元也就指的是Goroutine

参考链接: https://blog.csdn.net/qq_33296108/article/details/82791691
  • client如何实现长连接
server是设置超时时间,for循环遍历的
  • 主协程如何等其余协程完再操作
使用channel进行通信,context,select

参考链接: https://www.cnblogs.com/secondtonone1/p/11803961.html
  • slice,len,cap,共享,扩容
append函数,因为slice底层数据结构是,由数组、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的数组
  • map如何顺序读取
map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。
  • 实现set
参考链接:  http://www.zzvips.com/article/70251.html
  • 实现消息队列(多生产者,多消费者)
使用切片加锁可以实现

参考链接: https://www.cnblogs.com/asong2020/articles/13697817.html
  • 大文件排序
归并排序,分而治之,拆分为小文件,在排序

参考链接: https://zhuanlan.zhihu.com/p/124356219
  • 基本排序,哪些是稳定的
快速排序、希尔排序、堆排序、直接选择bai排序不是稳定的排序算法。

基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

链接地址: https://zhuanlan.zhihu.com/p/83756377
  • http get跟head
HEAD: 只请求页面的首部。

GET: 请求指定的页面信息,并返回实体主体。

POST: 请求服务器接受所指定的文档作为对所标识的URL的新的从属实体。

参考链接: https://blog.csdn.net/ysh1042436059/article/details/80985574
  • http 401,403
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝

参考链接: https://www.cnblogs.com/purewhite/p/10871851.html
  • http keep-alive
另一个问题就是在使用keepalive的情况,客户端依然有同时发送多个请求的情况,比如网页加载是需要同时load多个静态资源。比如 浏览器默认最大连接数是6,现在有十个资源同时加载,那么这十个里会有6个并行,4个与前6个串行。

在keepalive里有个问题就是如果能知道每个repose与其对应的request的话,并发的请求可以只需要一次TCP连接,这也就是http2.0实现的多路复用。

参考链接: https://www.jianshu.com/p/347416aafd3f
  • http能不能一次连接多次请求,不等后端返回
http本质上市使用socket连接,因此发送请求,接写入tcp缓冲,是可以多次进行的,这也是http是无状态的原因
  • tcp与udp区别,udp优点,适用场景
tcp传输的是数据流,而udp是数据包,tcp会经过三次握手,udp不需要

参考链接: https://www.cnblogs.com/GuoXinxin/p/11657676.html
  • time-wait的作用
主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。

参考链接: https://www.cnblogs.com/li-hao/archive/2011/12/08/2280678.html
参考地址: https://blog.csdn.net/qq_16077957/article/details/80112397
  • 数据库如何建索引
索引的优点如下:

快速访问数据表中的特定信息,提高检索速度。
创建唯一性索引,保证数据表中每一行数据的唯一性。
加速表与表之间的连接。
使用分组和排序进行数据检索时,可以显著减少查询中分组和排序的时间。
索引的缺点:

虽然提高了的查询速度,但却降低了更新表的速度,比如 update、insert,因为更新数据时,MySQL 不仅要更新数据,还要更新索引文件;
建立索引会占用磁盘文件的索引文件。

参考链接: https://www.cnblogs.com/lianhaifeng/p/13544893.html
  • 孤儿进程,僵尸进程
我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

参考链接: https://www.cnblogs.com/yadongliang/p/13407666.html
  • 死锁条件,如何避免
死锁条件:
互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

如何避免:
我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。

1 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
2 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
3 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

参考链接: https://www.cnblogs.com/bopo/p/9228834.html
  • linux命令,查看端口占用,cpu负载,内存占用,如何发送信号给一个进程
参考链接: https://www.cnblogs.com/Paul-watermelon/p/10407502.html
参考链接: https://www.cnblogs.com/mufengforward/p/9282889.html
  • git文件版本,使用顺序,merge跟rebase
git rebase 你其实可以把它理解成是“重新设置基线”,将你的当前分支重新设置开始点。这个时候才能知道你当前分支于你需要比较的分支之间的差异。
原理很简单:rebase需要基于一个分支来设置你当前的分支的基线,这基线就是当前分支的开始时间轴向后移动到最新的跟踪分支的最后面,这样你的当前分支就是最新的跟踪分支。这里的操作是基于文件事务处理的,所以你不用怕中间失败会影响文件的一致性。在中间的过程中你可以随时取消rebase 事务。

参考链接: https://www.jianshu.com/p/4079284dd970

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:流雨声

查看原文:Golang后端面试汇总-001

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

765 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传