「刷起來」Go必看的進階面試題詳解

2023-04-04 21:01:03

勤學如春起之苗,不見其增日有所長;輟學如磨刀之石,不見其損日有所虧。

本文的重點:逃逸分析、延遲語句、雜湊表、通道、介面。

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",表示所有的資料已經讀取完畢。

因為通道是一種同步的資料傳輸方式,寫入和讀取會阻塞直到對方準備好,所以輸出的結果應該是:

需要注意的是:在通道被關閉後,讀取操作仍然可以從通道中讀取到之前寫入的資料。這是因為通道中的資料並沒有立即消失,而是在讀取完畢後被垃圾回收器回收。因此,在使用通道時,需要根據實際情況判斷何時關閉通道,以避免出現不必要的競態和記憶體漏失。

5.介面

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!",然後被輸出。

因此,輸出的結果應該是:


需要注意的是,介面是一種動態型別,它可以包含任何實現了它所定義的方法集的型別。在使用介面時,需要注意以下幾點:

  1. 介面是一種參照型別的資料結構,它的值可以為nil。
  2. 實現介面的型別必須實現介面中所有的方法,否則會編譯錯誤。
  3. 介面的值可以賦給實現介面的型別的變數,反之亦然。
  4. 在實現介面的型別的方法中,可以通過型別斷言來判斷介面值的實際型別和值。

總結

這篇文章總結了5個知識點的面試題:逃逸分析、延遲語句defer、雜湊表map、通道Channel、介面interface

下一篇文章計劃分享的5個知識點是:unsafe、context、錯誤處理、計時器、反射。

歡迎大家三連支援一波,你的點贊、分享,是我更文的最大動力。

公眾號:程式設計師升職加薪之旅