golang进阶必知问题

golang进阶必知问题_第1张图片

目录

  • 1、简述GMP模型
  • 2、go的逃逸分析算法
  • 3、go的垃圾回收及相关的优化
  • 4、锁与channel都可以实现并发安全,使用时如何选择?
  • 5、golang如何应用开闭原则?
  • 6、是否遇到过并发问题,如何解决的?
  • 7、进程、线程、协程的概念和区别?
  • 8、map、slice、channel的底层实现?
  • 9、从两百万个字符的slice中快速查找我们需要的子串
  • 10、本地队列和全局队列的区别?从队列中取出goroutine是否需要加锁?
  • 11、如何通过context实现控制超时操作?
  • 12、sync.Map 和 map + mutex有何区别?

1、简述GMP模型

Go(也称为 Golang)是一种编程语言,它在并发编程方面具有独特的设计和模型,其中 GMP 模型是其并发模型的一部分。

GMP 模型是 Go 语言的并发调度模型,它包含三个主要的组件:

1.1. Goroutines:

  • Goroutines 是 Go 语言中轻量级的并发执行单位。与传统的线程相比,Goroutines 更加轻量且消耗较小的资源,可以高效地创建大量的并发任务。
  • Goroutines 使用关键字 go 来创建,可以在函数或方法前面加上 go 关键字来启动一个 Goroutine。例如:go myFunction()
  • Goroutines 由 Go 语言的运行时系统(Runtime)进行调度,实现了自动的任务切换和并发管理。

1.2. Channels:

  • Channels 是 Goroutines 之间通信的主要方式。它是一种类型安全的、阻塞式的通信机制,用于在不同的 Goroutines 之间传递数据。
  • 通过 make(chan Type) 来创建一个 Channel,可以在 Goroutines 之间使用 <- 操作符发送和接收数据。例如:myChannel <- datadata <- myChannel
  • Channels 可以用于同步 Goroutines 的执行顺序,实现数据共享和通信。

1.3. Scheduler:

  • Go 语言的运行时系统具有一个调度器,负责管理和调度 Goroutines 的执行。
  • 调度器使用称为 G-M-P 模型的架构。G 代表 Goroutines,M 代表操作系统的线程,P 代表处理器。其中,P 用于绑定 M 和 Goroutines,协调任务的执行。
  • 调度器根据一定的策略(例如工作窃取、抢占式调度等)来分配 Goroutines 给 M,以实现并发的调度。

总之,GMP 模型是 Go 语言并发编程的核心,通过轻量级的 Goroutines 和通信机制 Channels,以及调度器的协调管理,使得并发编程在 Go 中变得更加简单和高效。这种模型使得开发者能够利用多核处理器的优势,编写出高效且易于理解的并发代码。

2、go的逃逸分析算法

Go 语言的逃逸分析(Escape Analysis)是一种编译器优化技术,用于确定程序中的变量是否在函数栈上分配内存,还是需要在堆上分配内存。逃逸分析的目标是最小化堆内存的分配,从而提高程序的性能和内存利用率。

逃逸分析的算法通常分为以下几个步骤:

a. 标记阶段(Marking Phase):
在编译阶段,编译器会对函数进行标记,记录每个变量是否逃逸。如果变量在函数内部使用且不会被返回,它就可能被分配在栈上,否则可能被分配在堆上。

b. 逃逸分析阶段(Escape Analysis Phase):
编译器会分析函数内部的变量,检查它们的作用域和生命周期。如果发现变量在函数外部被引用(逃逸),则可能将其分配在堆上,以确保其在函数退出后仍然有效。

c. 决策阶段(Decision Phase):
根据逃逸分析的结果,编译器会做出决策,将变量分配在栈上还是堆上。栈上分配速度更快,但生命周期受限;堆上分配生命周期更长,但速度较慢。

逃逸分析的优点包括:

  • 减少堆内存分配:通过在栈上分配对象,减少了垃圾回收的压力和堆内存的碎片。
  • 提高局部性:栈上分配可以提高数据的局部性,从而提高缓存命中率。
  • 改善性能:避免了频繁的堆内存分配和回收操作,提高了程序的性能。

逃逸分析在 Go 语言中是默认开启的,并且在编译器的优化阶段自动进行。在编写代码时,开发者不需要显式指定变量的分配位置,编译器会根据逃逸分析的结果自动进行优化。

总之,Go 语言的逃逸分析算法是一项重要的编译器优化技术,可以帮助提高程序的性能和内存利用率,同时减少垃圾回收的开销。

可以在编译时加参数 -gcflags '-m -l ’ 查看有哪些变量发生了逃逸。例如:go build -gcflags '-m -l 'main.go

3、go的垃圾回收及相关的优化

go的垃圾回收采用的是标记清除(1.3版本)或三色标记(1.5版本)算法。
垃圾回收时会发生STW,造成卡顿,如果垃圾回收频率太高则会占用很大比例的CPU运行时间,不利于程序整体的吞吐。
垃圾回收发生的时机是,比如设置GOGC=100(这也是默认值),则新增内存达到上次垃圾回收后存活量的%100时发生垃圾回收。例如上次回收后,剩余1GB,则新增达到1GB,总共2GB时会再发生一次垃圾回收。
降低频率的设置可以把GOGC设置大一些,如1000,但如果每次的存活量差别较大,会有很大的不确定性,可能会造成OOM。
也可以使用ballast方案,这是官方推荐的方案,即GOGC使用默认值,然后初始化一个生命周期贯穿整个go程序生命周期的超大slice,如2GB~10GB,
这个slice只占虚拟内存,不占用物理内存,且会被GC线程(垃圾回收器)统计到。

背景知识:
go1.3 版本之前使用的是 标记-清除 算法,但是执行该算法时,需要停掉用户程序(STW),即保证 GC 和 用户程序 是串行 的。这种算法简单,但是会严重影响用户程序的执行。
go1.5 版本开始后提出了 三色标记法,它优化了标记过程,同时结合 屏障技术 极大的缩短了 STW 的时间,让 用户程序 和 GC 在 标记和清除阶段 可以并行运行。
并发的标记、清除算法已经把STW的时间大幅降低了,Go 垃圾回收的耗时还是主要取决于标记花费的时间的长短,清除过程是非常快的。
GOGC 设置的非常小,会频繁触发 GC 导致太多无效的 CPU 浪费,反应到程序的表现就会特别明显。

4、锁与channel都可以实现并发安全,使用时如何选择?

根据实际需求,如果需要多个goroutine的业务逻辑协作,偏重于数据的传递,选择channel,例如调度
如果不需要且对性能要求较高,且读取操作比写操作多,选RWmutex。
如果试图保护某个结构的内部状态,选mutex。如调度时worker的选择和任务的指派,会先上锁。

PS golang的互斥锁和读写锁
a. 互斥锁(Mutex):
互斥锁用于实现悲观锁的机制,它可以确保同一时刻只有一个 Goroutine 能够访问被保护的临界区。
b. 读写锁(RWMutex):
读写锁用于实现乐观锁的机制,允许多个 Goroutine 同时读取共享资源,但只允许一个 Goroutine 写入共享资源。

需要根据具体的需求来选择使用互斥锁还是读写锁。互斥锁适用于对共享资源的修改操作,但可能限制并发性能;读写锁适用于多读少写的场景,可以提高并发读取性能。

5、golang如何应用开闭原则?

开闭原则是面向对象设计中的一个重要原则,它指出软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。在 Go 语言中,应用开闭原则可以通过以下几种方式实现:

5.1. 使用接口抽象:
定义接口来描述不同实现的共同行为,然后在代码中使用这些接口。当需要新增一种实现时,只需要实现接口并注册到程序中,而不需要修改已有的代码。

5.2. 使用函数回调:
将变化的部分封装成函数,并将这些函数作为参数传递给其他函数。这种方式允许通过传递不同的函数实现来实现不同的行为,从而在不修改现有代码的情况下扩展功能。

6、是否遇到过并发问题,如何解决的?

7、进程、线程、协程的概念和区别?

8、map、slice、channel的底层实现?

在 Go 语言中,map、slice 和 channel 都是非常重要的数据结构,它们在底层的实现上有一些共同点和差异。

Map 的底层实现:
Go 的 map 是基于散列表(hash table)实现的,用于存储键值对。底层使用了哈希函数来映射键到特定的桶(bucket),每个桶存储一个链表或红黑树,用于解决哈希冲突。在插入、查询和删除操作时,通过哈希函数来定位桶,然后在桶内进行操作。

Slice 的底层实现:
Go 的 slice 是对数组的一个封装,底层是一个数组指针和长度信息。当创建一个 slice 时,会分配一个底层数组,并记录 slice 的长度和容量。如果 slice 需要扩展,会创建一个新的底层数组,并将数据复制到新的数组中,以便支持可变长度。这种动态数组的特性使得 slice 在性能和内存利用方面都有良好的表现。

Channel 的底层实现:
Go 的 channel 是用于 Goroutine 之间通信的管道,底层实现依赖于同步原语。通常使用一个等待队列(wait queue)来管理数据的发送和接收,以及一个缓冲区来存储数据。在无缓冲通道中,发送和接收操作会直接进行 Goroutine 的切换,实现同步通信。在有缓冲通道中,数据会先存储在缓冲区中,直到缓冲区满或者空时才会进行切换。

需要注意的是,这些底层实现细节可能会因 Go 版本和编译器优化而有所变化。这些数据结构在实际使用中非常常见,但了解底层实现并不是使用它们的必要前提。在编写 Go 代码时,重点关注它们的使用和性能特点更为重要。

9、从两百万个字符的slice中快速查找我们需要的子串

使用第三方库提供的高效查找算法。
例如实现了Boyer-Moore 算法的库,实现了KMP算法的库。

10、本地队列和全局队列的区别?从队列中取出goroutine是否需要加锁?

在 Go 语言中,"本地队列"和"全局队列"通常指的是调度器(Scheduler)中的工作队列,用于管理待执行的 Goroutine。在 Go 的运行时调度器中,这些队列起着重要的作用,以实现并发执行和任务调度。

本地队列(Local Queue):

每个逻辑处理器(P)都有一个本地队列,用于存储待执行的 Goroutine。
当一个 Goroutine被创建或者被调度器从其他 P 中抢占时,会被放入本地队列中。
本地队列的访问不需要加锁,因为每个 P 拥有自己的本地队列,不会出现并发访问的情况。

全局队列(Global Queue):

所有逻辑处理器共享一个全局队列,用于存储一部分待执行的 Goroutine。
当本地队列空闲时,会从全局队列中获取一部分 Goroutine 来执行。
全局队列的访问可能需要加锁,因为多个逻辑处理器可能同时竞争全局队列的资源。
从队列中取出 Goroutine 是否需要加锁取决于是从本地队列还是全局队列中取出。在从本地队列中取出时,因为每个逻辑处理器都有自己的本地队列,无需加锁。而在从全局队列中取出时,可能需要对全局队列进行加锁,以确保多个逻辑处理器之间的竞争安全。

11、如何通过context实现控制超时操作?

使用 context.WithTimeout 函数创建一个带有超时的 context.Context 对象。这个函数接受一个父级 Context 和一个超时时间作为参数,返回一个新的 Context 和一个 cancel 函数,用于取消操作。

12、sync.Map 和 map + mutex有何区别?

sync.Map 是 Go 语言标准库提供的一种并发安全的映射类型,它在内部使用了一些优化的机制来降低锁的竞争,从而提高性能。以下是 sync.Map 内部的一些优化机制:

12.1. 分片锁(Sharding Locks):
sync.Map 内部将整个映射分为多个片段,每个片段有自己的锁。这样可以降低锁的竞争,因为每个 Goroutine 在操作映射时只需要锁定特定的片段,而不会影响其他片段的操作。

12.2. 读写分离:
sync.Map 中,读操作和写操作是分开的。读操作可以同时进行,不需要加锁。写操作则需要使用锁来保护。这样可以在保证并发性的同时,提高读操作的性能。

12.3. 自旋锁(Spin Locks):
sync.Map 使用了一种自旋锁的技术,当发生轻微的竞争时,Goroutine 会进行自旋等待,而不是立即放弃 CPU 时间片。这可以在短时间内减少锁的竞争,提高性能。

12.4. MapEntry 的延迟删除:
sync.Map 中的键值对是通过 MapEntry 结构表示的。当执行删除操作时,并不会立即删除 MapEntry,而是将其标记为删除状态。这些标记的删除会在后续操作中进行清理,避免了在删除时立即进行昂贵的内存操作。

总之,sync.Map 利用分片锁、读写分离、自旋锁等优化机制,使得在高并发的情况下,对于读写操作的性能表现相对较好。当需要在并发环境中使用映射时,可以考虑使用 sync.Map 来获得更好的性能和并发安全性。

使用普通的 map 数据结构时,如果需要在多个 Goroutine 中进行并发访问,通常需要使用 mutex 来保护 map 的读写操作,以避免并发问题。
这种方式虽然可以实现并发安全,但在高并发情况下,由于整个 map 使用一个锁进行保护,可能会导致锁的竞争,从而影响性能。
使用 map 加 mutex 的方式可以适用于一些简单的场景,但在高并发和大规模数据集的情况下,可能会成为性能瓶颈。

你可能感兴趣的:(golang)