最新公告
  • 欢迎您光临码农资源网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!加入我们
  • 精选常见go语言面试题及答案解析

    随着云计算时代愈演愈烈,Go 语言的应用也越来越广泛,已然成为首选编程语言。而且,薪资也水涨船高,并且都是急聘。为啥?因为现在熟练掌握 Go 语言的人才少,看到趋势的人才太少,这个赛道还没有十分拥挤,机会也在日益增多。

    1. make 与 new的区别

    知识点

    主要在对于go的使用层面的理解new与make的功能

    回答

    都是是内建函数

    new: 的作用是初始化一个指向类型的指针(*T),使用 new 函数来分配空间。传递给 new 函数的是一个类型,不是一个值。返回值是指向这个新分配的零值的指针。

    make:的作用是为 slice,map 或 chan 初始化并返回引用 (T),第一个参数是一个类型,第二个参数是长度;返回值是一个类型。

    make(T, args) 函数的目的与 new(T) 不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是T*)的一个初始化的(不是零值)的实例

    2. main 与 init的区别

    知识点

    对main方法与init方法的执行,细节的考虑;也需要考虑包的引用

    回答

    1. 数量对比:对于当前项目包来说只能存在1个main、而init可以存在多个;

    2. 编译上:程序在编译的时候会先加载相关依赖包的init函数,再执行main中的init函数;

    3. 作用:main用于启动整个程序,init用于程序执行前做包的初始化函数;

    4. 注意细节:一个包可以出线多个 init() 函数,一个源文件也可以包含多个 init() 函数,但是同一个包中多个 init()函数的执行顺序没有明确定义,但是不同包的init函数是根据包导入的依赖关系决定的;

    5. 使用上:init() 函数在代码中不能被显示调用、不能被引用(赋值给函数变量),否则出现编译错误;

    6. 一个包被引用多次,如 A import B,C import B,A import C,B 被引用多次,但 B 包只会初始化一次;

    7. 引入包,不可出现死循坏。即 A import B,B import A,这种情况编译失败;

    8. 在项目开发中,对于整个项目需要尽量避免使用init因为会与其他依赖阐述;

    3. 如何控制并发

    知识点

    对go的运用,其中包含协程的运行和结束,以及使用协程的注意细节;其中也包含channel、sync、协程的运用和理解;

    回答

    1. 可以构建协程池,利用协程池解决问题

    2. 利用clannel、sync、原子包

    4. go defer(for defer)的执行

    先进后出,后进先出

    5. select可以用于什么?

    知识点

    考察对select的运用以及理解

    回答

    常用于gorotine的完美退出 golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。

    6. context包的用途

    知识点

    考察对Context的运用以及理解。

    回答

    Context通常被译作上下文,它是一个比较抽象的概念,其本质,是【上下上下】存在上下层的传递,上会把内容传递给下。在Go语言中,程序单元也就指的是Goroutine

    而它的设计除了上下层的信息传递之外,还可以用与控制协程的退出,结合select

    7. map如何顺序读取

    知识点

    这是一个干扰题,在实际中很少运用,考察map与切片的理解

    回答

    map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。

    8. Slice与数组区别,Slice底层结构

    知识点

    考察切片与数组的理解

    回答

    切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。

    切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

    切片对象非常小,是因为它是只有3个字段的数据结构:

    • 指向底层数组的指针
    • 切片的长度
    • 切片的容量

    这3个字段,就是Go语言操作底层数组的元数据。

    Go语言操作底层数组的元数据

    9. Golang Slice的扩容机制,有什么注意点

    Go 中切片扩容的策略是这样的:

    首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量。

    否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍。

    否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4 , 直到最终容量大于等于新申请的容量。

    如果最终容量计算值溢出,则最终容量就是新申请容量。

    情况一:

    原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。

    情况二:

    原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行append() 操作。这种情况丝毫不影响原数组。

    要复制一个Slice,最好使用Copy函数。

    10. 退出程序时怎么防止channel没有消费完

    知识点

    考察对channel的使用情况

    回答

    这个问题可以通过参数记录channel的任务量,根据获取channel的返回结果减一,归零的时候则完成;或者可以利用sync.waitGroup

    11. sync.Pool用过吗,为什么使用

    这是对象池子,我们可以通过它提供的New方法指定我们需要创建的对象,然后再返回

    12. go 中除了加 Mutex 锁以外还有哪些方式安全读写共享变量?

    Go 中 Goroutine 可以通过 Channel 进行安全读写共享变量。

    13. Go中对nil的Slice和空Slice的处理是一致的吗?

    首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。

    • slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的。
    • slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值

    14. goroutine泄漏有没有处理

    知识点

    主要是观察你是否对协程的使用有细节考虑

    回答

    首先需分析:泄露的原因主要存在的问题在于协程没有释放,也就是协程没有运行结束

    解决:在协程内部可以通过context上下文控制创建的协程,也可以通过timeout结合select处理

    15. 进程,线程,协程的区别

    进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

    线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

    协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

    16. 垃圾回收

    垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。

    常见的垃圾回收算法:

    1、引用计数:每个对象维护一个引用计数,当被引用对象被创建或被赋值给其他对象时引用计数自动加 +1;如果这个对象被销毁,则计数 -1 ,当计数为 0 时,回收该对象。

    优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。

    缺点:不能很好的处理循环引用

    2、标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有被标记的则进行回收。

    优点:解决了引用计数的缺点。

    缺点:需要 STW(stop the world),暂时停止程序运行。

    3、分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。

    优点:回收性能好

    缺点:算法复杂

    4、三色标记法

    白色:可以清除的元素,视为垃圾

    灰色:可能是活跃元素,最终会转化为黑色,先是白色转为灰色

    黑色:则就是活跃元素

    规则

    • 初始状态下所有对象都是白色的。
    • 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象
    • 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。
    • 循环步骤3,直到灰色对象全部变黑色。
    • 通过写屏障(write-barrier)检测对象有变化,重复以上操作
    • 收集所有白色对象(垃圾)。

    STW(Stop The World)

    为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。

    STW对性能有一些影响,Golang目前已经可以做到1ms以下的STW。

    写屏障(Write Barrier)

    为了避免GC的过程中新修改的引用关系到GC的结果发生错误,我们需要进行STW。但是STW会影响程序的性能,所以我们要通过写屏障技术尽可能地缩短STW的时间。

    造成引用对象丢失的条件:
    一个黑色的节点A新增了指向白色节点C的引用,并且白色节点C没有除了A之外的其他灰色节点的引用,或者存在但是在GC过程中被删除了。以上两个条件需要同时满足:满足条件1时说明节点A已扫描完毕,A指向C的引用无法再被扫描到;满足条件2时说明白色节点C无其他灰色节点的引用了,即扫描结束后会被忽略 。

    写屏障破坏两个条件其一即可

    破坏条件1:Dijistra写屏障

    满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色

    破坏条件2:Yuasa写屏障

    满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏) 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色

    17、GPM 调度 和 CSP 模型

    GPM 分别是什么、分别有多少数量:

    G(Goroutine):即Go协程,每个go关键字都会创建一个协程。

    M(Machine):工作线程,在Go中称为Machine,数量对应真实的CPU数(真正干活的对象)。

    P(Processor):处理器(Go中定义的一个摡念,非CPU),包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数。

    M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。

    Goroutine调度策略:

    队列轮转:P 会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度。除此之外,P还会周期性的查看全局队列是否有G等待调度到M中执行。

    系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是M的缓存池,也可能是新建的。

    当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

    CSP 模型:

    CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型。

    18. 竞态、内存逃逸

    竞态

    资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加-race 标识可以检查代码中是否存在资源竞争。

    解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。

    sync.Mutex

    sync.RWMutex

    内存逃逸

    「逃逸分析」就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

    逃逸场景:

    • 指针逃逸
    • 栈空间不足逃逸
    • 动态类型逃逸
    • 闭包引用对象逃逸

    19. Go中对nil的Slice和空Slice的处理是一致的吗?

    首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。

    slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的。

    slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

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

    通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。

    21. channel 为什么它可以做到线程安全?

    Channel 可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。

    22. GC 的触发条件?

    主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。

    被动触发,分为两种方式:

    1. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC

    2. 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。

    23. 怎么查看Goroutine的数量?怎么限制Goroutine的数量?

    在Golang中,GOMAXPROCS中控制的是未被阻塞的所有Goroutine,可以被 Multiplex 到多少个线程上运行,通过GOMAXPROCS可以查看Goroutine的数量。

    使用通道。每次执行的go之前向通道写入值,直到通道满的时候就阻塞了。

    24. Channel是同步的还是异步的?

    Channel是异步进行的, channel存在3种状态:

    1、nil,未初始化的状态,只进行了声明,或者手动赋值为nil

    2、active,正常的channel,可读或者可写

    3、closed,已关闭,千万不要误认为关闭channel后,channel的值是nil

    操作 一个零值nil通道 一个非零值但已关闭的通道 一个非零值且尚未关闭的通道
    关闭 产生异常 产生异常 成功关闭
    发送数据 永久阻塞 产生异常 阻塞或者成功发送
    接收数据 永久阻塞 永不阻塞 阻塞或者成功接收

    25. Go的Slice如何扩容?

    在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。

    扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:

    若 slice 容量小于1024个元素,那么扩容的时候slice的cap就翻番;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。

    若 slice 容量够用,则将新元素追加进去,slice.len++,返回原 slice

    若 slice 容量不够用,将 slice 先扩容,扩容得到新 slice,将新元素追加进新 slice,slice.len++,返回新 slice。

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

    方法的接收者:

    1、值类型,既可以调用值接收者的方法,也可以调用指针接收者的方法;

    2、指针类型,既可以调用指针接收者的方法,也可以调用值接收者的方法。

    但是接口的实现,值类型接收者和指针类型接收者不一样:

    1、以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口;

    2、以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口。

    通常我们使用指针作为方法的接收者的理由:

    1、使用指针方法能够修改接收者指向的值。

    2、可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

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

    Goroutine 需要维护执行用户代码的上下文信息,在运行过程中需要消耗一定的内存来保存这类信息,如果一个程序持续不断地产生新的 goroutine,且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。

    28. Goroutine发生了泄漏如何检测?

    可以通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源。

    29. Go中两个Nil可能不相等吗?

    Go中两个Nil可能不相等。

    接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

    两个接口值比较时,会先比较 T,再比较 V。接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。

    func main() {
     var p *int = nil
     var i interface{} = p
     fmt.Println(i == p) // true
     fmt.Println(p == nil) // true
     fmt.Println(i == nil) // false
    }

    例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil),i与p作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil。

    但是当 i 与 nil 比较时,会将nil转换为接口 (T=nil, V=nil),与 i(T=*int, V=nil) 不相等,因此 i != nil。因此 V 为 nil ,
    但 T 不为 nil 的接口不等于 nil。

    30. Go语言中的内存对齐了解吗?

    CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。

    CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,例如:

    变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。

    也可以看到,内存对齐对实现变量的原子性操作也是有好处的,每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。

    简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。

    31. channel 实现原理,底层实现结构,怎么优雅的关闭一个 channel。channel 有哪些应用,什么情况下 channel 会造成内存泄露?( buf 是环形链表的数据结构)

    chan实现原理

    结构体:

    type hchan struct {
     qcount   uint  // 队列中的总元素个数
     dataqsiz uint  // 环形队列大小,即可存放元素的个数
     elemsize uint16  //每个元素的大小
     closed   uint32  //标识关闭状态
     elemtype *_type // 元素类型
        
     buf      unsafe.Pointer // 环形队列指针 - 
     
     sendx    uint   // 发送索引,元素写入时存放到队列中的位置
     recvx    uint   // 接收索引,元素从队列的该位置读出
        
     recvq    waitq  // 等待读消息的goroutine队列
     sendq    waitq  // 等待写消息的goroutine队列
        
     lock   mutex  //互斥锁,chan不允许并发读写
    }

    读写流程

    向 channel 写数据:

    1. 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。

    2. 若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。

    3. 若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。

    从 channel 读数据:

    1. 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。

    2. 如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。

    3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。

    4. 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。

    关闭 channel:

    1. 关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。

    panic 出现的场景还有:

    关闭值为 nil 的 channel

    关闭已经关闭的 channel

    向已经关闭的 channel 中写数据

    32. channel 关闭了接着 send 数据会发生什么,关闭一个已经关闭的 channel 会发生什么?

    读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内 关闭前 是否有元素而不同。

    如果 chan 关闭前, buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true 。

    如果 chan 关闭前, buffer 内有元素已经被读完, chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false 。

    写已经关闭的 chan 会 panic

    33. map 的底层实现,什么情况下会扩容,怎么扩容?是线程安全的吗?那 sync 包中的 map 是怎么实现线程安全的?

    map底层实现:

    map由结构体hmap构成

    type hmap struct {
      // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
      // Make sure this stays in sync with the compiler's definition.
      count     int // 哈希表中元素个数,即len(map)的返回值
      flags     uint8
      B         uint8  // 线性表中桶个数的的对数log_2(哈希表元素数量最大可达到装载因子*2^B)
      noverflow uint16 // 溢出桶的大概数字;详情见incrnoverflow
      hash0     uint32 // 哈希种子
      buckets    unsafe.Pointer // 指向线性表的指针,数组大小为2^B,如果元素个数为0,它为nil.
      oldbuckets unsafe.Pointer // 指向扩容后的老线性表地址
      nevacuate  uintptr        // 表示扩容进度
      extra *mapextra // 垃圾回收用
    }

    bmap结构体

    // A bucket for a Go map.
    type bmap struct {
      // tophash包含此桶中每个键的哈希值最高字节(高8位)信息(也就是前面所述的high-order bits)。
      // 如果tophash[0] < minTopHash,tophash[0]则代表桶的搬迁(evacuation)状态。 tophash [bucketCnt]uint8 }

    在顺序遍历时会用随机种子产生一个随机数,表示开始遍历的桶位置,因为随机数每次产生的数字可能都是不同的,

    所以每次for range得到的结果也是不同的。以下为初始化哈希迭代器的方法源码:

    map扩容方式

    有俩种方式会导致map的扩容,另一种是由于桶后面跟的链表太长所导致的扩容。

    载荷因子引起:当元素个数 >= 桶(bucket)总数 * 6.5,这时说明大部分桶都被占满了如果再来元素,大概率会发生哈希冲突。因此需要扩容,扩容方式为将 B + 1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets。该方法我们称之为增量扩容。如下图所示,插入25时会经历大量哈希冲突,再插入元素时6/8=0.67就需要扩容了。

    判断溢出桶是否太多,当桶总数 < 2 ^ 15 时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。当桶总数 >= 2 ^ 15 时,直接与 2 ^ 15 比较,当溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。buckets数量维持不变,将长度过长的溢出桶搬运到[]bmap的其他桶上,该方法我们称之为等量扩容。如下图所示,每一个溢出桶可以存八个元素,为了画图方便这里老虎就只当做只能存一个元素处理了,当插入元素41时,链表长度已经达到了5,如果哈希冲突过多,那么会最终演变为遍历访问链表,时间复杂度为O(n)的算法了!

    考点:sync.Map

    go1.9版本之后的 sync.Map , sync.map 通过原子操作的方式实现了并发安全,并采用读写分离的方式,提高了并发的性能。

    34. context 结构原理

    context 用途:

    Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。

    数据结构

    Context 只定义了接口,凡是实现该接口的类都可称为是一种 context。

    type Context interface {
       Deadline() (deadline time.Time, ok bool)
       Done() <-chan struct{} Err() error Value(key interface{}) interface{} }

    「Deadline」 方法:可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。

    「Done」 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。

    「Err」 方法:返回Context 被取消的原因。

    「Value」 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

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

    Go中map如果要实现顺序读取的话,可以先把map中的key,通过sort包排序。

    36. select+channel

    select 可以实现多路复用,即同时监听多个 channel。

    发现哪个 channel 有数据产生,就执行相应的 case 分支

    如果同时有多个 case 分支可以执行,则会随机选择一个

    如果一个 case 分支都不可执行,则 select 会一直等待

    select 实现原理:

    select 是 GO 语言中用来提供 IO 复用的机制,它可以检测多个 chan 是否 ready(可读/可写)。

    Go 实现 select 时,定义了一个数据结构表示每个 case 语句(包含defaut),select 执行过程可以类比成一个函数,函数输入 case 数组,输出选中的 case,然后程序流程转到选中的 case块。

    源码包 src/runtime/select.go 定义了表示case语句的数据结构:

    c : 表示当前 case 语句所操作的 channel 指针

    // Select case descriptor.
    // Known to compiler.
    // Changes here must also be made in src/cmd/internal/gc/select.go's scasetype.
    type scase struct {
     c    *hchan         // chan
     elem unsafe.Pointer // data element
    }

    elem :表示缓冲区地址

    总结

    • select 语句中除 default 外,每个 case 操作一个channel,要么读要么写
    • select语句中除 default 外,各 case 执行顺序是随机的
    • select 语句中如果没有 default 语句,则会阻塞等待任一 case
    • select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取

    37. Goroutine和线程的区别?

    一个线程可以有多个协程

    线程、进程都是同步机制,而协程是异步

    协程可以保留上一次调用时的状态,当过程重入时,相当于进入了上一次的调用状态

    协程是需要线程来承载运行的,所以协程并不能取代线程,「线程是被分割的CPU资源,协程是组织好的代码流程」

    38. 知道golang的内存逃逸吗?什么情况下会发生内存逃逸?

    能引起变量逃逸到堆上的典型情况:

    栈空间不足也会发生逃逸

    • 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此 其生命周期大于栈,则溢出。
    • 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。 所以编译器没法知道变量什么时候才会被释放。
    • * 在一个切片上存储指针或带指针的值。** 一个典型的例子就是 [] string 。这会导致切片的内容逃逸。尽管其后 面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
    • slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知 道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 在 interface 类型上调用方法。
    • 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知 道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上 分配。

    举例

    通过一个例子加深理解,接下来尝试下怎么通过 go build -gcflags=-m 查看逃逸的情况。

    /package main
    import "fmt"
    type A struct {
     s string
    }
    // 这是上面提到的 "在方法内把局部变量指针返回" 的情况
    func foo(s string) *A {
     a := new(A)
     a.s = s
     return a //返回局部变量a,在C语言中妥妥野指针,但在go则ok,但a会逃逸到堆
    }
    func main() {
     a := foo("hello")
     b := a.s + " world"
     c := b + "!"
     fmt.Println(c)
    }
    

    39. 拷贝大切片一定比小切片代价大吗?

    并不是,所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指 针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会 复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。

    SliceHeader 是切片在go的底层结构。

    type SliceHeader struct {
     Data uintptr
     Len  int
     Cap  int
    }

    大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。

    40. 拷贝大切片一定比小切片代价大吗?

    字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。那么问题来了。频繁的内存拷贝 操作听起来对性能不大友好。有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢?

    package main
    import (
     "fmt"
     "reflect"
     "unsafe"
    )
    func main() {
     a :="aaa"
     ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a))
     b := *(*[]byte)(unsafe.Pointer(&ssh))
     fmt.Printf("%v",b)
    }

    41. 这个代码会造成死循环吗?

    问题:

    package main
    import "fmt"
    func main() {
     s := []int{1,2,3,4,5}
     for _, v:=range s {
     s =append(s, v)
     fmt.Printf("len(s)=%vn",len(s))
     }
    }

    回答

    不会死循环,for range其实是golang的语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数 的循环。

    func main() {
     s := []int{1,2,3,4,5}
     for_temp := s
     len_temp := len(for_temp)
     for index_temp := 0; index_temp < len_temp; index_temp++ {
     value_temp := for_temp[index_temp]
     _ = index_temp
     value := value_temp
     // 以下是 original body
     s =append(s, value)
     fmt.Printf("len(s)=%vn",len(s))
     }
    }

    42. for循环select时,如果通道已经关闭会怎么样?

    如果select中的case只有一个, 又会怎么样? for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。 如果select里边只有一个case,而这个case被关闭了,则会出现死循环。

    43. 对已经关闭的的chan进行读写,会怎么样?为什么?

    读已经关闭的chan能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。

    • 如果chan关闭前,buffer内有元素还未读,会正确读到chan内的值,且返回的第二个bool值(是否读成功)为true。
    • 果chan关闭前,buffer内有元素已经被读完,chan内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个bool值一直为false。

    写已经关闭的chan会panic

    1.为什么写已经关闭的chan就会panic呢?

    因为在底层中对chansend的时候;当c.closed !=0则为通道关闭,此时执行写,源码提示直接panic,输出的内容就是上面提到的”send on closed channel”

    2. 为什么读已关闭的chan会一直能读到值?

    c.closed != 0 && c.qcount == 0指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值)

    如果接收值的地址ep不为空

    • 那接收值将获得是一个该类型的零值
    • typedmemclr 会根据类型清理相应地址的内存
    • 这就解释了上面代码为什么关闭的chan会返回对应类型的零值

    44. 对未初始化的的chan进行读写,会怎么样?为什么?

    读写未初始化的chan都会阻塞。

    45. 死锁产生的原因

    主要是主协程因为channel而被阻塞,就会报dead lock。

    死锁的情况有哪些

    1. 第一种:由于无缓冲channel必须同时写和读才能执行,所以当单个gorountine顺序执行的时候,channel的此性质会造成死锁

    2. 第二种:2个 以上的go程中, 使用同一个 channel 通信。 写入无缓冲channel先顺序执行的时候

    3. 第三种:2个以上的go程中,使用多个 channel 通信。 A go 程 获取channel 1 的同时,尝试使用channel 2, 同一时刻,B go 程 获取channel 2 的同时,尝试使用channel 1

    4. 第四种: 在go语言中, channel 和 读写锁、互斥锁 尽量避免交叉混用。——“隐形死锁”。如果必须使用。推荐借助“条件变量”

    死锁如何检查

    1. 利用pprof检查golang代码中的死锁

    2. 可能在应用中产生的因素在于rpc的调度,或者http调度,没有设置超时,导致远程交叉

    3. 可能是读写数据库,因为大量数据或者SQL写的有问题导致与数据库之间的连接出现问题(尽量设置好超时时间,或者考虑开辟一个新的协程去处理)

    4. 养成单元测试的习惯,一般简单死锁都可以通过测试解决

    46. channel的应用

    1. 通道同步

    2. 任务通道

    3. 生产者/消费者

    4. 并发控制

    5. 停止任务

    想要了解更多内容,请持续关注码农资源网,一起探索发现编程世界的无限可能!
    本站部分资源来源于网络,仅限用于学习和研究目的,请勿用于其他用途。
    如有侵权请发送邮件至1943759704@qq.com删除

    码农资源网 » 精选常见go语言面试题及答案解析
    • 7会员总数(位)
    • 25846资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 293稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情