7

「刷起来」Go必看的进阶面试题详解 - 王中阳Go1

 1 year ago
source link: https://www.cnblogs.com/wangzhongyang/p/17287281.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

勤学如春起之苗,不见其增日有所长;辍学如磨刀之石,不见其损日有所亏。

本文的重点:逃逸分析、延迟语句、散列表、通道、接口。

1.逃逸分析

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

问题描述:

有如下Go代码:

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

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

请问上面的代码中,变量x是否会发生逃逸?

答案解析:

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

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

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

更多逃逸分析的内容,可以阅读我之前分享的文章:内存分配和逃逸分析详解

2.延迟语句

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

问题描述:

有如下Go代码:

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语句的执行顺序和函数返回时的状态,避免出现不符合预期的结果。

3.散列表Map

Go语言中的map是一种非常有用的数据结构,可以用于存储键值对。下面是一道涉及map的面试题及其详解。

问题描述:

有如下Go代码:

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时,需要注意以下几点:

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

4.通道Channel

Go语言中的通道(channel)是一种非常有用的特性,用于在不同的goroutine之间传递数据。下面是一道涉及通道的面试题及其详解。

问题描述:

有如下Go代码:

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",表示所有的数据已经读取完毕。

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

2fb0d4b7-0b51-4c88-a803-89ea17a61bc8.png

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

Go语言中的接口(interface)是一种非常重要的特性,用于定义一组方法。下面是一道涉及接口的面试题及其详解。

问题描述:

有如下Go代码:

type Animal interface {
    Speak() string
}

type Dog struct{}

func (d *Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c *Cat) Speak() string {
    return "Meow!"
}

func main() {
    animals := []Animal{&Dog{}, &Cat{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

请问上面的代码中,输出的结果是什么?

答案解析:

在上面的代码中,我们定义了一个Animal接口,它有一个Speak方法。然后,我们定义了Dog和Cat两个结构体,分别实现了Animal接口的Speak方法。

接着,在main函数中,我们创建了一个Animal类型的切片,其中包含了一个Dog对象和一个Cat对象。然后,我们使用for循环遍历这个切片,调用每个对象的Speak方法,并输出它们返回的字符串。

因为Dog和Cat都实现了Animal接口的Speak方法,所以它们都是Animal类型的对象,可以被放入Animal类型的切片中。在遍历切片时,我们调用每个对象的Speak方法,它们分别返回"Woof!"和"Meow!",然后被输出。

因此,输出的结果应该是:

41740f8a-b9f3-4265-8959-07a34a710ad8.png

需要注意的是,接口是一种动态类型,它可以包含任何实现了它所定义的方法集的类型。在使用接口时,需要注意以下几点:

  1. 接口是一种引用类型的数据结构,它的值可以为nil。
  2. 实现接口的类型必须实现接口中所有的方法,否则会编译错误。
  3. 接口的值可以赋给实现接口的类型的变量,反之亦然。
  4. 在实现接口的类型的方法中,可以通过类型断言来判断接口值的实际类型和值。

这篇文章总结了5个知识点的面试题:逃逸分析、延迟语句defer、散列表map、通道Channel、接口interface

下一篇文章计划分享的5个知识点是:unsafe、context、错误处理、计时器、反射。

欢迎大家三连支持一波,你的点赞、分享,是我更文的最大动力。

公众号:程序员升职加薪之旅


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK