Go語言內幕(1):主要概念與專案結構

yhx發表於2015-09-07

這個系列部落格主要為那些對 Go 基本知識已經有一定了解,又希望對其內部細節進行更深一步地探索的人準備的。今天這一篇主要分析 Go 原始碼的基本結構以及 Go 編譯器的某些內部細節。讀完這篇部落格後,你會得到下面三個問題的答案:

1. Go 原始碼結構是什麼樣子的?
2. Go 編譯器是如何工作的?
3. Go 語言中的結點樹的基本結構是什麼樣的?

讓我們開始吧

每當開始學習一門新程式語言的時候,你總是可以找到大量的 “hello world” 教程、新手指南或者關於語言的主要概念、語法甚至標準庫的文件。然而,當你想找一些介紹得更加深入的資料,比如語言執行時分配的資料結構在記憶體中的佈局,或者呼叫一個內建函式時到底生成了什麼樣的彙編程式碼,你就會發現這並非易事。顯然,這些問題的答案都藏在原始碼中。但是,以我的個人經驗來看,你很可能花費數小時在原始碼中摸索卻最終一無所獲。

我並不是打算裝得自己什麼都懂,也沒有打算介紹得面面俱到。而是希望可以幫助你去探索 Go 語言的原始碼。

在我們開始之前,我們需要自己有一份 Go 原始碼的拷貝。要獲得它的原始碼非常容易,只需要執行如下程式碼:

請注意,這份程式碼的主分支是在不斷改進中的,我在這個部落格中使用的是 release-brach.go1.4 這個分支。

搞清楚專案結構

如果你看一下 Go 倉庫的 /src 資料夾,你會看到很多資料夾。其中,大部分資料夾都是 Go 標準庫的原始檔。該專案使用標準命名規則,所以每一個包(pakage)都在一個獨立的資料夾中,而且這個資料夾的名稱與包名稱相同。除了標準庫以外,該目錄中還有很多其它的東西。就我各人看法,其中最有用的檔案中主要有:

資料夾 描述
/src/cmd/ 包含不同的命令列工具。
/src/cmd/go/ 該目錄下包含一個 Go 工具的原始碼檔案。此工具用於下載編譯 Go 的原始檔,以及安裝 Go 語言的包。在完成上述工作中,它會收集所有原始檔並呼叫 Go 連結器與編譯器。
/src/cmd/dist/ 此目錄下也包含一個工具。此工具用於編譯生成所有其它命令列工具。同時,它會由標準庫生成所有的包。要想搞明白每個工具或者包到底用到了哪些庫,你就需要分析這裡的原始碼。
/src/cmd/gc/ 包含 Go 編譯器與系統架構無關的部分。
/src/cmd/ld/ 包含 Go 連結器與系統架構無關的部分。與系統架構相關的部分被放在以 l 開頭的目錄中。這些目錄的命名規則與編譯器部分的命名規則相同。
/src/cmd/5a/, 6a, 8a, and 9a 此目錄下存放針對不同架構的 Go 語言彙編編譯器。Go 彙編程式的語言並不能一一對應地對映到下層機器的組合語言。不過,對於每種不同的架構都存在一個將 Go 彙編程式翻譯為機器彙編程式的編譯器。你可以這這裡找到更多內容。
/src/lib9/, /src/libbio, /src/liblink 在編譯器、連結器、以及執行時中用到的不同庫。
/src/runtime/ 這部分包含了 Go 語言最重要的包,所有程式都預設匯入這些包。其中包括所有的執行時功能,比如記憶體管理、垃圾回收、Go 協程(goroutine)等等。

Go 編譯器內部機制

正如提到的那樣,Go 編譯器中與系統結構無關的部分被放在 /src/cmd/gc 目錄下。其入口點在 lex.c 檔案中。除了一些共同的部分,比如命令列引數解析,編譯器還要完成如下的工作:

1. 初始化一些通用資料結構。

2. 遍歷提供的所有 Go 原始碼檔案,並針對每個檔案呼叫 yyparse 方法。該方法會完成真正的語法分析。Go 編譯器使用 Bison 作為程式分析生成器。語法描述儲存在檔案 go.y 中(後面我會提供詳細的說明)。最終,這一步會生成一個完整的分析樹,其中每個結點表示編譯後程式的一個元素。

3. 遞規地遍歷生成的樹,並做出一定修改,例如為那些應當隱式定義的節點指定型別資訊、重寫在執行時包中傳遞給函式的某些語言元素——如型別轉換,以及其它一些工作。

4. 語法解析樹處理完成後,再執行真正的編譯,將結點翻譯成彙編程式碼。

5. 在磁碟上建立目標檔案,並將翻譯生成的彙編程式碼以及一些額外的資料結構,如符號表等,寫入目標檔案中。

深入 Go 語言語法

現在讓我們再進一步。 go.y 檔案中包含了語言的語法規則,所以這個檔案是一個探索 Go 編譯器的很好突破口,也是我們理解語言語法規則的關鍵。這個檔案主要包括如下幾部分:

這個宣告中定義了 xfndcl 以及 fundcl 兩個結點。 fundcl 結點可以有以下兩種形式。第一種對應於如下的語法結構:

其第二種形式對應於下面這種語法結構:

xfndcl 結點中包含儲存於常量 LFUNC 中的關鍵字 func,以及其後的 fndcl 與 fnbody 結點。

Bison(或者說 Yacc)語法一個十份重要的特徵是,它允許將任意 C 程式碼放在結點定義之後。每當在原始碼檔案中找到匹配該結點定義的部分的時候,相應的 C 程式碼就會執行。這裡,我們把最終結果結點定義為 $ $,其子結點分別為 $1,$2……

通過一個例子更加容易理解。注意下面這段簡化後的程式碼:

首先,我們建立了一個新結點,該結點包含函式宣告的型別資訊。同時,此結點的引數列表引用了結點 $3,結果列表引用了結點 $5。隨後建立了結果結點 $ $。在結果結點中儲存了函式的名稱和以及其型別結點。 正如你所看到的那樣,在 go.y 檔案中的定義與結點結構之間可能沒有直接的對應關係。

如何理解結點

是時候看一下結點到底是什麼東西了。首先,結點是一個結構體(你可以在這裡找到其定義)。這個結構體包含了大量的屬性,這是因為它需要各種不同型別的結點型別,而不同類別的結點又有著不同的屬性。下面列出了一些我認為比較重要一些屬性:

結點結構體域 描述
op 結點操作符。每個結點都有這個域。它將不同型別的結點區分開來。在前面的例子中,該域分別是 OTFUNC(操作型別函式)與 ODCLFUNC(操作宣告函式)。
type 該域引用一個包含型別資訊的結構體(有些結點沒有型別資訊,例如,像 if、switch、for 之類的控制流語句)。
val 在表示常量的結點中,該域儲存常量值。

到目前為止,你已經明白了結點樹的基本結構了,你可以去運用一下這些知識。在接下來的博文中,我們會用一個簡單的 Go 應用作為例項來分析 Go 編譯器到底是如何編譯程式碼的。

相關文章