Go 語言函數的幕後:從符號表到棧幀

Go 函數是構建 Go 程序的基本模塊,我們每天都在使用它們,但你是否想過 Go 函數在編譯和運行時是如何工作的呢?本文將深入探討 Go 函數的內部機制,從符號表到棧幀,揭示 Go 函數運行的奧祕。

函數的命名和符號表

在 Go 中,每個函數都有一個唯一的名稱,這是因爲 Go 編譯器會創建一個符號表來記錄所有變量和函數的名稱。當我們在代碼中定義一個函數時,它的名稱會被添加到符號表中。如果兩個函數擁有相同的名稱,就會導致衝突,因爲符號表中只能存在一個相同名稱的條目。

func a() {

}

func a(b string) {

}

//a redeclared in this block is the error I get

那麼,如何查看 Go 程序的符號表呢?

我們可以使用 go tool nm 命令來查看 Go 可執行文件的符號表。例如,假設我們有一個名爲 main 的 Go 程序,我們可以使用以下命令生成符號表:

go tool nm ./main &> logs.txt

這會將符號表信息輸出到 logs.txt 文件中。符號表中每個條目包含三個部分:地址類型名稱

100343920 T main.getURL
1003439b0 T main.main
100343f30 T main.main.func1
100343fd0 T main.main.func1.Println.1
100343d80 T main.main.func2

符號類型說明:

從符號表中我們可以看到,全局變量和函數存儲在編譯後的二進制文件的數據段中,而函數的實際代碼則存儲在文本段中,文本段包含程序的可執行代碼。

當一個函數被調用時,指令指針會跳轉到文本段中函數代碼的位置。

導出與非導出標識符

在 Go 中,標識符(變量或函數)的名稱如果以大寫字母開頭,則可以被其他包訪問,稱爲導出標識符;如果以小寫字母開頭,則只能在定義它的包內訪問,稱爲非導出標識符

例如,以下代碼中,Apple 函數可以被其他包訪問,而 apple 函數只能在當前包中訪問。

func Apple() {
    fmt.Println("id")
}

func apple() {
    fmt.Println("id")
}

Go 編譯器會根據標識符的名稱來決定它是否可以被導出。

局部作用域與全局作用域

除了導出與非導出標識符之外,我們還需要了解 Go 中的局部變量和全局變量。

全局變量在函數之外定義,可以在整個程序範圍內訪問。局部變量則在函數內部定義,只能在函數內部訪問。

var globalVar int = 10

func myFunc() {
    localVar := 20
    // ...
}

在上面的代碼中,globalVar 是一個全局變量,可以在任何地方訪問;而 localVar 是一個局部變量,只能在 myFunc 函數內部訪問。

函數調用和棧幀

當一個函數被調用時,Go 運行時會創建一個棧幀來存儲函數的局部變量、參數和返回值。棧幀是一個內存區域,用於存儲函數執行期間所需的所有信息。

棧幀的結構:

  1. 函數參數: 傳遞給函數的參數會被存儲在棧幀中。

  2. 局部變量: 在函數內部聲明的局部變量也會被存儲在棧幀中。

  3. 返回值: 函數執行完畢後,返回值也會被存儲在棧幀中。

  4. 返回地址: 函數執行完畢後,需要返回到調用它的位置,這個位置的地址被存儲在棧幀中。

棧幀的創建和銷燬:

棧幀的管理:

例如,以下代碼展示了函數調用和棧幀的創建過程:

func main() {
    tempFunc := func(count int) int {
        return count + 1
    }
    tempVal := tempFunc(0)
    fmt.Println(tempVal)
}

main 函數調用 tempFunc 函數時,會創建一個新的棧幀來存儲 tempFunc 函數的局部變量、參數和返回值。

局部變量的內存管理:

局部變量在函數執行期間存儲在棧幀中。當函數執行完畢時,棧幀會被銷燬,局部變量也會隨之消失。

總結

Go 函數的內部機制涉及到符號表、棧幀、局部變量和全局變量等概念。理解這些概念對於深入理解 Go 程序的運行機制至關重要。通過本文的介紹,相信你對 Go 函數的工作原理有了更深入的瞭解。

拓展

希望這篇文章能幫助你更好地理解 Go 函數的內部機制。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FOBDtXXq1fkv5Mh4TY5xZQ