Golang 面试问题总结 I (make, channel, Goroutine)

Author: Xcourse   2023-Mar-17 14:57   Reads: 2035

欢迎加入微信工作内部分享群,每天发布新的精选高薪工作。

官方邮箱:enquiry@xcourse.sg

微信分享群:@新加坡工作内部分享群

WhatsApp群:@Singapore Jobs & Internships

Telegram中文群:@新加坡工作内部分享群

Telegram英文群:@Singapore Jobs

------------------------------------------------------------------------------------------------------

 

下一篇:Golang 面试问题总结 II (Concurrency)

下一篇:Golang面试问题总结 III - GMP,三色法,STW,GC,GC优化

 

1. 与其他语言相比,使用 Go 有什么好处

⚫ 与其他作为学术实验开始的语言不同,Go代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。

⚫ Golang 针对并发进行了优化,并且在规模上运行良好。

⚫ 由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。

⚫ 自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。

 

2. Golang 使用什么数据类型

Golang 使用以下类型:

⚫ Method

⚫ Boolean

⚫ Numeric

⚫ String

⚫ Array

⚫ Slice

⚫ Struct

⚫ Pointer

⚫ Function

⚫ Interface

⚫ Map

⚫ Channel

 

3. Go 程序中的包是什么

包 (pkg) 是 Go 工作区中包含 Go 源文件或其他包的目录。源文件中的每个函数、变量和类型都存储在链接包中。每个 Go 源文件都属于一个包,该包在文件顶部使用以下命令声明:

package < packagename >

GoCopy

您可以使用以下方法导入和导出包以重用导出的函数或类型:

import <packagename>

GoCopy

Golang 的标准包是 fmt,其中包含格式化和打印功能,如 Println().

 

4. Go 支持什么形式的类型转换

将整数转换为浮点数。 Go 支持显式类型转换以满足其严格的类型要求。

i := 55 //int

j := 67.8 //float64

sum := i + int(j) //j is converted to int

 

5. 什么是 Goroutine

你如何停止它? 一个 Goroutine 是一个函数或方法执行同时旁边其他任何够程采用了特殊的Goroutine 线程。Goroutine 线程比标准线程更轻量级,大多数 Golang 程序 同时使用数千个 g、Goroutine。

要创建 Goroutine,请 go 在函数声明之前添加关键字。

go f(x, y, z)

GoCopy

您可以通过向 Goroutine 发送一个信号通道来停止它。Goroutines 只能在被告知检查时响应信号,因此您需要在逻辑位置(例如 for 循环顶部)包含检 查。

package main func main() {

 quit := make(chan bool)

 go func() {

   for {

       select {

       case <-quit:

           return

       default:

           // …

       }

 }

}()

// …

quit <- true

}

 

6. 如何在运行时检查变量类型

类型开关是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case,用作条件语句,和一个defaultcase,如果没有一个 case 为真,则执行。

 

7. Go 两个接口之间可以存在什么关系

如果两个接口有相同的方法列表,那么他们就是等价的,可以相互赋值。如果接口 A 的方法列表是接口 B 的方法列表的自己,那么接口 B 可以赋值给接口A。接口查询是否成功,要在运行期才能够确定。

 

8. Go 当中同步锁有什么特点 作用是什么

当一个 Goroutine(协程)获得了 Mutex 后,其他 Gorouline(协程)就只能乖乖的等待,除非该 gorouline 释放了该 MutexRWMutex 在读锁占用的情况下,会阻止写,但不阻止读 RWMutex 在写锁占用情况下,会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占 同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱, 保证系统的稳定性。

 

9. Go 语言当中 Channel(通道)有什么特点,需要注意什么

如果给一个 nil 的 channel 发送数据,会造成永远阻塞如果从一个 nil 的 channel 中接收数据,也会造成永久爱阻塞给一个已经关闭的 channel 发送数据, 会引起 pannic 从一个已经关闭的 channel 接收数据, 如果缓冲区中为 空,则返回一个零值

 

10. Go 语言当中 Channel 缓冲有什么特点

无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的。

 

11. Go 语言中 cap 函数可以作用于那些内容

cap 函数在讲引用的问题中已经提到,可以作用于的类型有:

⚫ array(数组)

⚫ slice(切片)

⚫ channel(通道)

 

12. go convey 是什么?一般用来做什么

⚫ go convey 是一个支持 golang 的单元测试框架

⚫ go convey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出 到 Web 界面

⚫ go convey 提供了丰富的断言简化测试用例的编写

 

13. Go 语言当中 new 和 make 有什么区别吗

new 的作用是初始化一个纸箱类型的指针 new 函数是内建函数,函数定义:

  • func new(Type) *Type

⚫ 使用 new 函数来分配空间

⚫ 传递给 new 函数的是一个类型,而不是一个值

⚫ 返回值是指向这个新非配的地址的指针

 

14. Go 语言中 make 的作用是什么

make 的作用是为 slice, map or chan 的初始化 然后返回引用 make 函数是内建函数,函数定义:

  • func make(Type, size IntegerType) Type

make(T, args)函数的目的和 new(T)不同 仅仅用于创建 slice, map,  channel 而且返回类西行是实例

 

15. Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同

虽然这三个函数,都是格式化输出,但是输出的目标不一样。Printf 是标准输出,一般是屏幕,也可以重定向。 Sprintf()是把格式化字符串输出到指定的字符串中。 Fprintf()是吧格式化字符串输出到文件中。

 

16. Go 语言当中数组和切片的区别是什么

数组:

数组固定长度数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型数组需要指定大小,不指定也会根据处初始化对的自动推算出大小,不可改变数组是通过值传递的

切片:

切片可以改变长度切片是轻量级的数据结构,三个属性,指针,长度,容量不需要指定大小切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容。

 

17. Go 语言当中值传递和地址传递(引用传递)如何运用 有什么区别 举例说明

  1. 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。
  2. 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。

 

18. Go 语言当中数组和切片在传递的时候的区别是什么

  1. 数组是值传递
  2. 切片是引用传递

 

19. Go 语言是如何实现切片扩容的

func main() {

   arr := make([]int, 0)

   for i := 0; i < 2000; i++ {

       fmt.Println("len 为", len(arr), "cap 为", cap(arr))

       arr = append(arr, i)

   }

}

我们可以看下结果依次是

0,1,2,4,8,16,32,64,128,256,512,1024

但到了 1024 之后,就变成了1024,1280,1696,2304

每次都是扩容了四分之一左右

 

20. 看下面代码的 defer 的执行顺序是什么? defer 的作用和特 点是什么

defer 的作用是:

你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

defer 的常用场景:

⚫ defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。

⚫ 通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

⚫ 释放资源的 defer 应该直接跟在请求资源的语句后。

 

21. Golang Slice 的底层实现

切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。

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

⚫ 指向底层数组的指针

⚫ 切片的长度

⚫ 切片的容量

 

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

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

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

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

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

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

 

23. 扩容前后的 Slice 是否相同

情况一:

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

情况二:

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

 

24. Golang 的参数传递、引用类型

Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等 这些),这样就可以修改原内容数据。

Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分 配内存和初始化成员结构,返回对象而非指针。

 

25. Golang Map 底层实现

Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。

 

26. Golang Map 如何扩容

装载因子:count/2^B

触发条件:

装填因子是否大于 6.5

overflow bucket 是否太多

解决方法:

双倍扩容:扩容采取了一种称为“渐进式”地方式,原有的key 并不会一次性搬迁完毕,每次最多只会搬迁 2个bucket

等量扩容:重新排列,极端情况下,重新排列也解决不了,map 成了链表,性能大大降低,此时哈希种子 hash0 的设置,可以降低此类极端场景的发生

 

27. Golang Map 查找

Go 语言中 map 采用的是哈希查找表,由一个 key 通过哈希函数得到哈希值,64位系统中就生成一个 64bit 的哈希值,由这个哈希值将 key 对应到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突。key 经过 hash 后共 64 位,根据 hmap 中 B 的值,计算它到底要落在哪个桶时,桶的数量为 2^B,如 B=5,那么用 64 位最后 5 位表示第几号桶,在用 hash 值的高 8 位确定在 bucket 中的存储位置,当前 bmap 中的 bucket 未找到,则查询对应的 overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。

如果两个不同的 key 落在的同一个桶上,hash 冲突使用链表法接近,遍历 bucket 中的 key 如果当前处于 map 进行了扩容,处于数据搬移状态,则优先从 oldbuckets 查找。

 

28. 介绍一下 Channel

Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。

所以 channel 收发遵循先进先出 FIFO,分为有缓存和无缓存,channel 中大致有 buffer(当缓冲区大小部位 0 时,是个 ring buffer)、sendx 和 recvx 收发的位置(ring buffer 记录实现)、sendq、recvq 当前 channel 因为缓冲区不足 而阻塞的队列、使用双向链表存储、还有一个 mutex 锁控制并发、其他原属等。

 

29. Go 语言的 Channel 特性

  1. 给一个 nil channel 发送数据,造成永远阻塞
  2. 从一个 nil channel 接收数据,造成永远阻塞
  3. 给一个已经关闭的 channel 发送数据,引起 panic
  4. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  5. 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
  6. 关闭一个 nil channel 将会发生 panic

 

30. Channel 的 ring buffer 实现

channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ring  buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。 在 channel 中,ring buffer 的实现如下:

image-20220223201510486

hchan 中有两个与 buffer 相关的变量:recvx 和 sendx。其中 sendx 表示 buffer 中可写的 index,recvx 表示 buffer 中可读的 index。 从 recvx 到 sendx 之间的元素,表示已正常存放入 buffer 中的数据。 我们可以直接使用 buf[recvx]来读取到队列的第一个元素,使用 buf[sendx] =  x 来将元素放到队尾。

 


Tags: interview golang backend

Topics: 面经