golang 面试知识收集

go的语言特点

协程调度
同步编程模式
非阻塞 I/O
I/O 多路复用
  1. 什么是内核态和用户态?
操作系统为了安全把CPU分成了特权模式和用户模式,也就是内核态和用户态,在操作敏感地带时,只能由内核操作,或者用户向内核请求,内核操作完了传给用户,也就是说用户的操作是受限的,仅能操作一些不敏感的东西,
在实际操作系统概念中就叫做内核态,在CPU设计上则叫做特权模式;我们叫做应用程序状态的,在实际操作系统概念中叫做用户态,CPU设计上叫做用户模式 
  1. 对线程模型有多少了解?
线程的实现曾有3种模型:
多对一(M:1)的用户级线程模型
一对一(1:1)的内核级线程模型
多对多(M:N)的两级线程模型

线程模型有n:
  内核线程模型
  用户级线程模型
  混合型线程模型
  1. 进程、线程、协程的区别?
进程是资源分配的最小单位(CPU时间片、内存等资源)
线程又叫做轻量级进程,由操作系统调度,是CPU调度的最小单位,一个进程可以有多个线程,最少有一个线程,但一个线程只能有一个进程
协程是协作式的用户态线程

进程和线程是抢占式执行,协程需要要用户手动来切换
进程面向的主要内容是内存分配管理,而线程主要面向的CPU调度
线程的切换有上下文切换且会保存到CPU的栈里。协程拥有自己的寄存器上下文和栈,无上下文切换,复用的就是它所在的线程的内存空间
协程不是被操作系统内核所管理,而完全是由程序所控制
线程是被动挂起恢复,协程是主动挂起恢复
线程的大小一般是2M,协程只有2K
  1. 谈谈GMP模型.
G(Goroutine) : 每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数
M(Machine): 对OS内核级线程的封装,数量对应真实的CPU数(真正干活的对象).
P (Processor): 逻辑处理器,即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数。   
  1. 空struct的常见用法.
1.占位符,不占内存
2.实现set集合
3.channel传参
4.只包含方法的空结构体
  1. defer常见用途.
资源释放(http client释放、锁释放、文件关闭)
调用栈,后入先出
recover捕获.   
  1. 什么是IO多路复用?
    多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。
    select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器
    内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符

  2. 谈谈golang的锁.
    互斥锁mutex和读写锁rwmutex.

  3. 什么是nil切片和空切片,有什么区别?
    空切片就是长度为0的切片,指针指向底层数组的地址len=0,容量有指向的底层数组决定, make([]int,0),可以append,且没有分配底层数组。
    nil切片简单来说就是切片的零值。指针并不指向底层的数组,而是指向一个没有实际意义的地址,len = 0 且 cap = 0 如 var s []int,未初始化不可以赋值,但是可以append,append 会初始化 nil slice,与此类似的函数还有 copy 。这两个函数内部都进行 make 初始化。每次对 slice 的操作内部是会产生一个新的数组,然后返回。

  4. goroutine 什么时候发生阻塞?

1.channel 在等待网络请求或者数据操作的IO返回的时候会发生阻塞
2.发生一次系统调用等待返回结果的时候
3.goroutine进行sleep操作的时候
  1. 内存泄漏常见场景?

暂时性:

  • 获取长字符串中的一段导致长字符串未释放
  • 获取长slice中的一段导致长slice未释放
  • 在长slice新建slice导致泄漏.

永久性:

  • goroutine永久阻塞而导致泄漏
  • time.Ticker未关闭导致泄漏
  • 不正确使用Finalizer导致泄漏
  • defer close忘记添加,导致文件句柄耗尽.

12.Mutex的模式?

  • 正常模式:锁的等待者会按照先进先出的顺序获取锁,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式。
  • 饥饿模式:互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待

13.谈谈http三次握手,四次挥手
握手:

  1. 客户端发起请求服务端(说明自己有发送能力)
  2. 服务端确认客户端有发送能力,确认客户端的接收能力
  3. 客户端确认接收能力正常.

挥手:

  1. 客户端请求断开
  2. 服务端确认
  3. 服务端确认数据发送完成
  4. 客户端确认已断开

14.RPC和http区别.
RPC:通过socket调用另一个socket服务

  1. RPC要求服务提供方和服务调用方都需要使用相同的技术,而http无需关注语言的实现,它不关心实现细节,跨平台、跨语言更灵活
  2. RPC的开发要求较多,RPC实现较为复杂
  3. RPC可以基于TCP和UDP,而http只能基于TCP
  4. 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿
  5. RPC面向C/S HTTP面向B/S.

15.REST特点.

  • 服务端专注数据存储,客户端关注于展示
  • 无状态(stateless),客户端保存会话信息,每次请求必须包括所有信息
  • 所有服务端响应都要被标为可缓存或不可缓存
  • 接口设计尽可能统一通用,且接口与实现解耦,使前后端可以独立开发迭代
  • 分层系统 客户端无法分辨代理或者真实服务器,还有安全层、负载均衡、缓存层
  • 按需代码.

16.linux中常见的进程间通讯方式.

1.管道pipe,
2.命名管道FIFO,
3.消息队列,
4.共享内存,
5.信号量,
6.套接字,
7.信号,
高级的有 dbus等

17.make和new的区别?

make返回的是引用类型,new返回的是指针类型

18.简述map的原理

type hmap struct {
     count     int    // 元素的个数
     B         uint8  // buckets 数组的长度就是 2^B 个
     overflow uint16 // 溢出桶的数量
 ​
     buckets    unsafe.Pointer // 2^B个桶对应的数组指针
     oldbuckets unsafe.Pointer  // 发生扩容时,记录扩容前的buckets数组指针
 ​
     extra *mapextra //用于保存溢出桶的地址
 }
在go的map实现中,它的底层结构体是hmap,hmap里维护着若干个bucket数组 (即桶数组)。

Bucket数组中每个元素都是bmap结构,也即每个bucket(桶)都是bmap结构,每个桶中保存了8个kv对,如果8个满了,又来了一个key落在了这个桶里,会使用overflow连接下一个桶(溢出桶)。
假设当前 B=4 即桶数量为2^B=16个

GET获取数据
①计算k4的hash值
②通过最后的“B”位来确定在哪号桶
③根据k4对应的hash值前8位快速确定是在这个桶的哪个位置
④对比key完整的hash是否匹配
⑤如果都没有找到,就去连接的下一个溢出桶中找

PUT存放数据
①通过key的hash值后“B”位确定是哪一个桶
② 遍历当前桶,通过key的tophash和hash值,防止key重复,然后找到第一个可以插入的位置,即空位置处存储数据。
③如果当前桶元素已满,会通过overflow链接创建一个新的桶,来存储数据。当两个不同的 key 落在同一个桶中,就是发生了哈希冲突。冲突的解决手段是采用链表法:在 桶 中,从前往后找到第一个空位进行插入。如果8个kv满了,那么当前桶就会连接到下一个溢出桶(bmap)

扩容的方式
扩容有两种,一种是等量扩容,另一种是2倍扩容
等量扩容:元素会发生重排,但不会换桶
2倍扩容: 当前桶数组确实不够用了,发生这种扩容时,元素会重排,可能会发生桶迁移。

扩容条件
装载因子(loadFactor)
loadFactor:=count / (2^B) 即 装载因子 = map中元素的个数 / map中当前桶的个数
通过计算公式我们可以得知,装载因子是指当前map中,每个桶中的平均元素个数。
扩容条件1:装载因子 > 6.5 (源码中定义的)
扩容条件2: 溢出桶的数量过多
扩容时的细节
1.在我们的hmap结构中有一个oldbuckets吗,扩容刚发生时,会先将老数据存到这个里面。
2.每次对map进行删改操作时,会触发从oldbucket中迁移到bucket的操作【非一次性,分多次】
3.在扩容没有完全迁移完成之前,每次get或者put遍历数据时,都会先遍历oldbuckets,然后再遍历buckets。   

19.go什么时候会死锁?

1.无缓冲的channel,读与写没配对准备好(包括单channel自己读写、单channel写的时候新协程未准备好读、多channel时要求对方先读/写)
2.读写锁相互阻塞,形成隐形死锁

20.context的原理和使用场景?

goroutine的相关环境快照,其中包含函数调用以及涉及的相关的变量值。拓扑结构是一个树型关系。
当一个 context 被取消时,从它派生的所有 context 也将被取消。
当一个context找元素时,会往父级context一级一级往上查找元素,知道根节点或者找到为止。
场景:
  1.请求链路传值。
  2.取消耗时操作,及时释放资源。
  3.防止 goroutine 泄露,主动取消未完成的任务。

因为每次设置都会基于父context创建新树,不会覆盖,所以线程安全

21.channel的实现原理?

缓冲链表
发送索引
接收索引
互斥锁
接收队列
发送队列
线程安全是因为底层基于锁做控制

22.slice的扩容机制?

容量小于1024时两倍扩容,大于1024时1.25倍扩容

23.谈谈Go内存管理

  • 1.内存池,Go 的程序在启动之初, 会一次性从OS那里申请一大块内存作为内存池,使用时是如下图一级一级申请一级一级还
  • 2.逃逸分析,逃逸到堆中的变量需要自己去手动清理,速度没有栈中元素清理速度快(由系统管理,使用起来高效无感知)
  • 3.垃圾回收

24.Go垃圾回收机制?

三色标记法
1.首先创建三个集合:白、灰、黑。
2.将所有对象放入白色集合中。
3.从根节点开始扫描,把遍历到的对象从白色集合放入灰色集合。
4.之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
5.重复4直至灰色对象为空
6.收集所有白色对象(垃圾)

25.Go语言的并发模型?

1.内核级线程
2.用户级线程
3.两级线程模型(混合线程模型)
4.GMP模型

26.谈谈沾包拆包的理解?

UDP没有粘包只有丢包, TCP粘包的原因 
TCP是基于流式的发送 并且存在丢包重发机制 TCP是可靠连接而UDP是不可靠的,TCP连接建立后可以一直不停的发送并没有明确的边界定义,会开辟一个缓冲区 发送端往其中写入数据 每过一段时间就发送出去 然后接收端接收到这些数据,有可能后续发送的数据和之前发送的数据同时存在缓冲区中随后一起发送(是粘包的一种形式 ),另一种是应用程序没有及时处理缓冲区中的数据 那么后续到达的数据会继续存放到缓冲区中 也就是2次接收的数据同时存在缓冲区中 下次取缓冲区的时候就会取出2次粘包后的数据 这是粘包的另外一种形式。填充缓冲区到一半缓冲区满了直接发送了 但是其实那个包还没填充完全 这个就是不完整的粘包了

27.TCP为什么是可靠的?

序列号和确认应答信号
超时重发控制
连接管理(三次握手四次挥手)
滑动窗口控制
流量控制
拥塞控制,

28.发布订阅模式和观察者模式有什么区别?
发布订阅模式是面向中间渠道开发的,依赖中间服务才能交互,观察者模式有更高的耦合。

29.mysql为什么要使用B+树存储索引?
因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低

30.分布式锁实现方式

1.数据库 
  1.唯一索引
  2.乐观锁 版本号
  3.悲观锁
2.redis setnx 或者 set key value nx px
3.etcd lock
4.zoomkeeper

31.高并发情况下使用缓存需要注意什么?

1.设置有效期
2.提高缓存命中率(缓存预热注意有效期要有随机事件波动防止缓存雪崩、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率)
3.缓存并发(设置lock,排队等待获取缓存结果)。
4.防止缓存穿透 缓存空结果避免错判,或者布隆过滤器缓存所有结果。
5.注意缓存一致性(数据库和缓存不一致),未命中缓存后判断锁是否存在,不存在去判断设置锁存在不,不存在就设置锁,并去查询数据库,更新缓存,其他情况等待。
6.防重放:(登录之类的token、唯一索引、分布式锁、加密请求并且添加时间戳,统一mq异步处理等)
7.缓存一致性:(双删机制,修改数据库之前删,修改完成后再删)

32.分表后的ID怎么保证唯一性?

1.设定步长
2.分布式ID,UUID或者雪花算法
3.分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,如订单表订单ID

33.分表后非sharding_key的查询应该怎么处理?

1.添加映射关系表,比如商家要查询订单列表,添加商家和用户关系表,查询的时候先通过商家查询到用户列表,再通过user_id去查询。
2.宽表,(mysql不合适,可用数仓或者已完成的数据迁移到时序数据库,或者ES查询)
3.多线程查询,合并清洗

34.谈谈GRPC模式

1. 简单模式:简单模式只是使用参数和返回值作为服务器与客户端传递数据的方式,最简单。
2. 客户端流模式:即从客户端往服务器端发送数据使用的是流,即服务器端的参数为流类型,然而在服务器相应后返还数据给客户端,使用的也是流的send方...
3.服务器端流模式:即服务器端返回结果的时候使用的是流模式,即传入的数据是通过参数形式传入的。但是在往客户端发送数据时使用send方法,与客户端...
4.双向模式:客户端如果不适用协程,那么发送必须在接收之前。如果使用协程,发送与接收并没有先后顺序。为了保证协程的同步,可以使用互斥量进行约束

35.https 原理

1.客户端去CA平台获取公钥
2.客户端生成随机数,根据公钥加密随机数生成报文
3.客户端将报文发送给服务端
4.服务端根据私钥解密报文获取随机数,向客户端确认
5.以上是非对称加密,后面服务端和客户端根据随机数对称加密http报文交互
原因:非对称加密的加解密效率是非常低的,前面的非对称加密是为了防止中间人攻击,
抓包流程:生成一个证书,用户需要手动把证书安装到客户端中,然后终端发起的所有请求通过该证书完成与抓包工具的交互,然后抓包工具再转发请求到服务器,最后把服务器返回的结果在控制台输出后再返回给终端,从而完成整个请求的闭环。

36.linux中常见的进程间通讯方式

1.管道pipe,
2.命名管道FIFO,
3.消息队列,
4.共享内存,
5.信号量,
6.套接字,
7.信号,
8.dbus等

37.浏览器打开网页的步骤

1.解析 URL
2.浏览器封装 HTTP 请求报文
3.DNS 域名解析获取 IP 地址
4. 建立 TCP 连接
5. 浏览器发送请求
6.负责传输的 IP 协议
7.使用 ARP 协议凭借 MAC 地址通信
8.服务器响应请求
9.断开 TCP 连接
10.浏览器渲染显示界面

38.接口幂等性怎么保证?也就是防重放

1.前端提交时每次推送唯一token,防重放攻击,ID被消费了就丢弃
2.前端添加按钮一次性点击或者等待结果成功响应才能继续点击的功能
3.添加分布式锁(setNx 必须得有过期时间和每次设置时的随机值,只能删除自己设置的锁)
4.mq异步队列单线程消费,重复的直接丢弃
5.设置状态机(状态流转 1->2->3->4)
6.数据库添加悲观锁, select for update
7.数据库添加乐观锁,查询之后返回版本号,版本号不匹配则取消操作
8.update添加条件 
9.加唯一索引
10.防重表(只有ID+唯一索引控制,删除业务数据的时候需要删除防重表数据)
11.insert on duplicate key update(如果不存在就插入,存在就更新)
12.inert ignore

39.网络链接流程

1.和客户端建立链接socket accept;
2.从 socket 种读取请求 recv;
3.解析客户端发送的请求 parse;
4.执行 get 指令;
5.响应客户端数据,也就是 向 socket 写回数据。

40.sync.pool使用需要注意哪些?

内存泄漏(没有正确的释放比如 slice map 等,对象存在自动扩容的机制时会出现,比如字符串的扩容之后占用空间没有释放会留到下一次使用时仍占用太多空间, 数据库连接/TCP连接也不适合放入,需要主动关闭)

会被GC清理掉(当对象在池中空闲一段时间后,可能会被垃圾回收器清除掉)

无法限制池中对象的数量

每次获取需要初始化(池中对象没有初始化)

41.goroutine发生调度的场景:

channel 阻塞

系统调用

系统监控(超过10ms或P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度)

主动让出  runtime.Gosched() 

Golang 的调度器会根据需要创建和销毁 P 和 M,然后将 G 分配给 P 来执行。当一个 P 上的协程阻塞时,调度器会将其从 P 上移除,并将其放入全局队列中,然后选择一个空闲的 P 来执行其他协程。当一个协程被阻塞解除时,调度器会将其重新放回到某个 P 上继续执行。除了以上的自动调度之外,还可以通过 runtime.Gosched() 函数来手动触发调度器的调度。这个函数会暂停当前协程的执行,并将其放回到队列中,然后调度器会选择一个协程继续执行。

goroutine的销毁是由Go运行时系统自动进行的,无需手动管理。当goroutine的函数执行完毕或者主线程退出时,对应的goroutine会被销毁

42.golang map 跨协程办法

sync.Map  
    Load Store LoadOrStore   delete range 遍历

sync.RWmutex

43.Golang中的错误处理机制是什么?

在Golang中,错误处理是通过返回错误对象来实现的。

44.在Golang中,多态性怎么实现的?

通过接口的实现来实现的。一个类型只需要实现了接口声明的所有方法,那么它就自动地实现了该接口。

45.slice数据结构

array:指向所引用的数组指针,可以说任意元素的指针

len:长度,当前引用切片的元素个数

cap:容量,当前引用切片的容量(底层数组的元素总数)
  1. 单引号,双引号 ,反引号区别
单引号 不能用来表示字符串

双引号 可解析的字符串字面量 (支持转义,但不能用来引用多行);

反引号 原生的字符串字面量 ,支持多行,不支持转义, 多用于书写多行消息、HTML以及正则表达式。

pattern:= `[a-zA-Z0-9\u2460-\u24FF]` //会报错

regexp.MustCompile(pattern)

dst := reg.ReplaceAllString(src, "*")

方法1: pattern := "[a-zA-Z0-9\u2460-\u24FF]"

方法2: pattern, _ = strconv.Unquote(`"` + pattern + `"`)

正则时尽量用双引号

其他示例: reg := regexp.MustCompile(`<h1 id="title2">(.*)</h1>`)   // 进行正则编译

47.docker 网络模式

bridge 桥接模式(默认)

host 主机模式 --net=host命令

none 无网络模式 --net=none

查看命令 docker network ls

48.压力测试工具

ab
wrk
hey
jmeter
postman

benchMark
Testify
benchstat
httpstat
fuzzing
gomock 

48.虚拟化和容器化

虚拟化   
帮助我们在单个物理服务器上运行和托管多个操作系统,VM完成了硬件层的抽象,提供的是虚拟机,每个VM都可以当作物理机
容器化 
提供了独立的的环境来运行应用程序,构成的是应用层的抽象,每个容器代表一个不同的应用

docker 是一种开放源码的应用容器引擎,是容器层面的
k8s 是一套自动化部署工具,可以管理docker容器,是容器编排层面的
(持久化可以用nfs(网络文件系统的卷)或者hostpath)

49.golang 结构体比较

同一个struct的2个实例
可比较的情况

1.结构体当中没有不可比较的类型(比如切片,数组,map)
2.必须是同一个结构体的实例

不同struct的2个实例,任何时候都不可比较

强制比较
reflect.DeepEqual(v1,v2)

50.如何在Go中循环中使用Defer

引入另一个函数来解决问题
for i := 0; i < 5; i++ {
    func(n int) {
        defer fmt.Println(n)
    }(i)
}

51.如何避免sync.Once失败后无法执行的问题?

package main

import (
   "sync"
   "sync/atomic"
)


type Once struct {
   done uint32
   m    sync.Mutex
}

func (o *Once) Do(f func() error) error {
   if atomic.LoadUint32(&o.done) == 0 {
      return o.doSlow(f)
   }
   return nil
}

func (o *Once) doSlow(f func() error) error {
   o.m.Lock()
   defer o.m.Unlock()
   var err error
   if o.done == 0 {
      err = f()
      // 只有没有 error 的时候,才修改 done 的值
      if err == nil {
         atomic.StoreUint32(&o.done, 1)
      }
   }
   return err
}

查看服务器连接情况(centos 安装net-tools)

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
或者 
ss -ta|wc -l; ss -tan state time-wait|wc -l

CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
ITMED_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT:另一边已初始化一个释放
LAST_ACK:等待所有分组死掉
posted @ 2022-07-31 22:26  雨落知音  阅读(382)  评论(0编辑  收藏  举报