八股文之go语言

miloyang
0 评论
/ /
313 阅读
/
5584 字
09 2024-01

面试问题100道:https://iswbm.com/golang-interview-qa

new和make的区别

1、make只能用来分配及初始化类型为slice、map、chan的数据;而new可以分配任意类型的数据。

2、new分配返回的是指针,即类型“*type”;而make返回引用,即type。

3、new分配的空间会被清零;make分配空间后,会进行初始化。

4、使用new来创建指针,并分配零值内存,使用make来创建切片、map和通道,发你被初始化的实例。

map

如何解决hash冲突

map 使用的是开放定址法(open addressing)解决哈希冲突的方法,当一个hash桶发生冲突的时候,会依次检查下一个位置,直到找到一个空槽。这个过程通常涉及到计算新位置并移动到那里,直到找到空槽或者遍历整个桶。

为什么是并发不安全的?

  • 非同步操作,因为map不提供任何同步机制,比如互斥锁来确保多个goroutine之间的安全访问,所以多个Goroutine并发读写同一个map美酒汇发生竞态条件
  • 扩容操作:当map内部的哈希表需要扩容的时候,会重新分配内存,重新计算hash值,并将旧的键值对复制到新的hash表中,如果在这个过程中有其他的goroutine同时进行读写操作,可能会导致数据不一致,甚至崩溃。
  • 无法保证原子性,map的读写操作并不是原子的,写入和读取是两个步骤,在并发环境中,可能导致一个Goroutine在另一个goroutine还没完成写入操作时读取到写入的数据。

所以如果要操作map,一般需要使用sync.Mutex来实现对map的安全访问。

逃逸分析

逃逸分析是Go语言中的一项重要优化技术,可以帮助程序减少内存分配和垃圾回收的开销,从而提高程序的性能。下面是一道涉及逃逸分析的面试题及其详解。
有如下Go代码:

func foo() *int {
    x := 1
    return &x
}

func main() {
    p := foo()
    fmt.Println(*p)
}

在上面的代码中,变量x只在函数foo()中被定义和初始化,然后其地址被返回给了主函数main()。因为返回值是指针类型,需要在堆上分配内存,所以变量x会发生逃逸。所谓逃逸,就是指变量的生命周期不仅限于函数栈帧,而是超出了函数的范围,需要在堆上分配内存。

如果变量x没有发生逃逸,那么它会被分配在函数栈帧中,随着函数的返回而被自动销毁。而如果发生了逃逸,变量x就需要在堆上分配内存,并由垃圾回收器负责回收。在实际的程序中,大量的逃逸会导致内存分配和垃圾回收的开销增加,从而影响程序的性能。

逃逸分析是Go语言的一项优化技术,可以在编译期间分析代码,确定变量的生命周期和分配位置,从而避免不必要的内存分配和垃圾回收。通过逃逸分析的优化,可以有效地提高程序的性能和可靠性。

延迟语句

defer语句是Go语言中的一项重要特性,可以用于在函数返回前执行一些清理或收尾工作,例如释放资源、关闭连接等。下面是一道涉及defer语句的面试题及其详解。

func main() {
    defer func() {
        fmt.Println("defer 1")
    }()
    defer func() {
        fmt.Println("defer 2")
    }()
    fmt.Println("main")
}

在上面的代码中,我们定义了两个defer语句,它们分别输出"defer 1"和"defer 2"。这两个defer语句的执行顺序是先进后出的,也就是说后定义的defer语句先执行,先定义的defer语句后执行。因此,输出的顺序应该是"main"、"defer 2"、"defer 1"。

这个例子也展示了defer语句的另一个特性,即在函数返回前执行。在main函数返回前,两个defer语句分别执行了它们的函数体,输出了相应的内容。这种特性可以用于释放资源、关闭连接等操作,在函数返回前保证它们被执行。

需要注意的是,defer语句并不是一种异步操作,它只是将被延迟执行的函数加入到一个栈中,在函数返回前按照后进先出的顺序执行。因此,在defer语句中的函数应该是轻量级的,避免影响程序的性能。同时,也需要注意defer语句的执行顺序和函数返回时的状态,避免出现不符合预期的结果。

Map

func main() {
    m := make(map[int]string)
    m[1] = "a"
    m[2] = "b"
    fmt.Println(m[1], m[2])
    delete(m, 2)
    fmt.Println(m[2])
}

在上面的代码中,我们使用make函数创建了一个map,然后向其中添加了两个键值对,分别是1:"a"和2:"b"。接着,我们输出了这两个键对应的值,分别是"a"和"b"。

接下来,我们使用delete函数从map中删除了键为2的元素。然后,我们尝试输出键为2的值,但是输出为空。这是因为我们已经从map中删除了键为2的元素,所以它对应的值已经不存在了。

需要注意的是,当我们从map中访问一个不存在的键时,它会返回该值类型的零值。在本例中,值的类型是string,它的零值是""。所以,当我们尝试输出键为2的值时,它返回的是空字符串。

需要提醒的是,map是一种引用类型的数据结构,它的底层实现是一个哈希表。在使用map时,需要注意以下几点:

map是无序的,即元素的顺序不固定。
map的键必须是可以进行相等性比较的类型,如int、string、指针等。(通俗来说就是可以用==和!=来比较的,除了slice、map、function这几个类型都可以)
map的值可以是任意类型,包括函数、结构体等。
在多个goroutine之间使用map时需要进行加锁,避免并发访问导致的竞态问题。

通道

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        ch <- 3
        close(ch)
    }()
    for {
        n, ok := <-ch
        if !ok {
            break
        }
        fmt.Println(n)
    }
    fmt.Println("done")
}

在上面的代码中,我们使用make函数创建了一个整型通道ch。然后,我们启动了一个goroutine,向通道中写入了三个整数1、2和3,并在最后使用close函数关闭了通道。

接着,在主函数中,我们使用for循环不断从通道中读取数据,直到通道被关闭。每次从通道中读取到一个整数后,我们将它输出。最后输出"done",表示所有的数据已经读取完毕。

因为通道是一种同步的数据传输方式,写入和读取会阻塞直到对方准备好,所以输出的结果应该是: 1/2/3/done

需要注意的是:在通道被关闭后,读取操作仍然可以从通道中读取到之前写入的数据。这是因为通道中的数据并没有立即消失,而是在读取完毕后被垃圾回收器回收。因此,在使用通道时,需要根据实际情况判断何时关闭通道,以避免出现不必要的竞态和内存泄漏。

说说GMP模型

我们知道,操作系统分为内核态和用户态,线程是运行在内核态的,是操作系统调度的最小单位,而协程是运行在用户态的,是一个逻辑的概念,go语言通过GMP模型,使得一个线程可以调度多个协程,一个协程也可以被多个线程调度。
好了,现在说说GMP,G就是代表着Goroutine,理解为我们通过go关键字来启动的一个协程,M就是machine,理解为内核态的一个线程,是非常珍贵的资源,P就是process,理解为调度器,是G和M承上启下的作用。

G一般都是存在在三个队列里面,一个是全局队列,一个是P的本地队列,一个是等待队列,当然具体如何存放,由哪一个线程进行执行,是由调度器调度的。

GC

如果 goroutine 一直占用资源怎么办,GMP模型怎么解决这个问题

如果有一个goroutine一直占用资源的话,GMP模型会从正常模式转为饥饿模式,通过信号协作强制处理在最前的 goroutine 去分配使用

如果若干个线程发生OOM,会发生什么?Goroutine中内存泄漏的发现与排查?项目出现过OOM吗,怎么解决?

线程 如果线程发生OOM,也就是内存溢出,发生OOM的线程会被kill掉,其它线程不受影响。

Goroutine中内存泄漏的发现与排查

go中的内存泄漏一般都是goroutine泄露,就是goroutine没有被关闭,或者没有添加超时控制,让goroutine一只处于阻塞状态,不能被GC。在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。

暂时性内存泄露,string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏。

永久性内存泄露,主要由goroutine永久阻塞而导致泄漏以及time.Ticker未关闭导致泄漏引起。

Go的垃圾回收算法

Go 现阶段采用的是通过三色标记清除扫法与混合写屏障GC策略。其核心优化思路就是尽量使得 STW(Stop The World) 的时间越来越短。

GC 的过程一共分为四个阶段: 栈扫描(STW),所有对象开始都是白色 从 root 开始找到所有可达对象(所有可以找到的对象),标记灰色,放入待处理队列 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色 清除(并发)循环步骤3 直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象。三色标记法相对于普通标记清除,减少了 STW 时间。这主要得益于标记过程是 “on-the-fly”的,在标记过程中是不需要 STW的,它与程序是并发执行的,这就大大缩短了 STW 的时间。

写屏障: 插入屏障, 在A对象引用B对象的时候,B对象被标记为灰色。(满足强三色不变性) 删除屏障,被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。(满足弱三色不变性)

混合写屏障: GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW), GC期间,任何在栈上创建的新对象,均为黑色。 被删除的对象标记为灰色。 被添加的对象标记为灰色。

人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步