Go 八股

1. 相比较于其他语言, Go 有什么优势或者特点?

  • Go 允许跨平台编译,编译出来的是二进制的可执行文件,直接部署在对应系统上即可运行。
  • Go 在语言层次上天生支持高并发,通过 goroutine 和 channel 实现。channel 的理论依据是 CSP 并发模型, 即所谓的通过通信来共享内存;Go 在 runtime 运行时里实现了属于自己的调度机制:GMP,降低了内核态和用户态的切换成本。
  • Go 的代码风格是强制性的统一,如果没有按照规定来,会编译不通过。

2. Golang 里的 GMP 模型?

GMP 模型是 golang 自己的一个调度模型,它抽象出了下面三个结构:

  • G: 也就是协程 goroutine,由 Go runtime 管理。我们可以认为它是用户级别的线程。
  • P: processor 处理器。每当有 goroutine 要创建时,会被添加到 P 上的 goroutine 本地队列上,如果 P 的本地队列已满,则会维护到全局队列里。
  • M: 系统线程。在 M 上有调度函数,它是真正的调度执行者,M 需要跟 P 绑定,并且会让 P 按下面的原则挑出个 goroutine 来执行:

优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取,如果全局队列也没有,会从其他的 P 上偷取 goroutine。

3. goroutine 的协程有什么特点,和线程相比?

goroutine 非常的轻量,初始分配只有 2KB,当栈空间不够用时,会自动扩容。同时,自身存储了执行 stack 信息,用于在调度时能恢复上下文信息。

而线程比较重,一般初始大小有几 MB(不同系统分配不同),线程是由操作系统调度,是操作系统的调度基本单位。而 golang 实现了自己的调度机制,goroutine 是它的调度基本单位。

4. Go 的垃圾回收机制?

Go 采用的是三色标记法,将内存里的对象分为了三种:

  • 白色对象:未被使用的对象;
  • 灰色对象:当前对象有引用对象,但是还没有对引用对象继续扫描过;
  • 黑色对象,对上面提到的灰色对象的引用对象已经全部扫描过了,下次不用再扫描它了。

当垃圾回收开始时,Go 会把根对象标记为灰色,其他对象标记为白色,然后从根对象遍历搜索,按照上面的定义去不断的对灰色对象进行扫描标记。当没有灰色对象时,表示所有对象已扫描过,然后就可以开始清除白色对象了。

5. go 的内存分配是怎么样的?

Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。具体的分配逻辑如下:

  • 当要分配大于 32K 的对象时,从 mheap 分配。
  • 当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
  • 当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。

6. channel 的内部实现是怎么样的?

channel 内部维护了两个 goroutine 队列,一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。

每当对 channel 的读写操作超过了可缓冲的 goroutine 数量,那么当前的 goroutine 就会被挂到对应的队列上,直到有其他 goroutine 执行了与之相反的读写操作,将它重新唤起。

7. 对已经关闭的 channel 进行读写,会怎么样?

当 channel 被关闭后,如果继续往里面写数据,程序会直接 panic 退出。如果是读取关闭后的 channel,不会产生 pannic,还可以读到数据。但关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。

为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。

 if v, ok := <-ch; !ok {
  fmt.Println("channel 已关闭,读取不到数据")
 }

还可以使用下面的写法不断的获取 channel 里的数据:

 for data := range ch {
  // get data dosomething
 }

这种用法会在读取完 channel 里的数据后就结束 for 循环,执行后面的代码。

8. map 为什么不是线程安全的?

map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。

如果我们想要并发安全的 map,则需要使用 sync.map。

9. map 的 key 为什么得是可比较类型的?

map 的 key、value 是存在 buckets 数组里的,每个 bucket 又可以容纳 8 个 key 和 8 个 value。当要插入一个新的 key - value 时,会对 key 进行 hash 运算得到一个 hash 值,然后根据 hash 值 的低几位(取几位取决于桶的数量,比如一开始桶的数量是 5,则取低 5 位)来决定命中哪个 bucket。

在命中某个 bucket 后,又会根据 hash 值的高 8 位来决定是 8 个 key 里的哪个位置。如果不巧,发生了 hash 冲突,即该位置上已经有其他 key 存在了,则会去其他空位置寻找插入。如果全都满了,则使用 overflow 指针指向一个新的 bucket,重复刚刚的寻找步骤。

从上面的流程可以看出,在判断 hash 冲突,即该位置是否已有其他 key 时,肯定是要进行比较的,所以 key 必须得是可比较类型的。像 slice、map、function 就不能作为 key。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计