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

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。本文由 伯樂線上 - yhx 翻譯,唐尤華 校稿。未經許可,禁止轉載!
英文出處:Sergey Matyukevich。歡迎加入翻譯組。

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

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

讓我們開始吧

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

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

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

git clone https://github.com/golang/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:     LFUNC fndcl fnbodyfndcl:     sym '(' oarg_type_list_ocomma ')' fnres| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres

這個聲明中定義了 xfndcl 以及 fndcl 兩個結點。 fndcl 結點可以有以下兩種形式。第一種對應於如下的文法結構:

somefunction(x int, y int) int

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

(t *SomeType) somefunction(x int, y int) int.

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

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

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

fndcl:      sym '(' oarg_type_list_ocomma ')' fnres        {          t = nod(OTFUNC, N, N);          t->list = $3;          t->rlist = $5;          $$ = nod(ODCLFUNC, N, N);          $$->nname = newname($1);          $$->nname->ntype = t;          declare($$->nname, PFUNC);      }| '(' oarg_type_list_ocomma ')' sym '(' oarg_type_list_ocomma ')' fnres

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

如何理解結點

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

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

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.