1. 程式人生 > >Go語言內幕(2):深入 Go 編譯器

Go語言內幕(2):深入 Go 編譯器

當你通過介面引用使用一個變數時,你知道 Go 執行時到底做了哪些工作嗎?這個問題並不容易回答。這是因為在 Go 中,一個型別實現了一個介面,但是這個型別並沒有包含任何對這個介面的引用。與上一篇部落格《Go語言內幕(1):主要概念與專案結構》一樣,你可以用 Go 編譯器的知識來回答這個問題。關於 Go 編譯器的內容我們已經在上一篇中已經討論過一部分了。

在這裡,讓我們更加深入地探索 Go 編譯器:建立一個簡單的 Go 程式來看一下 Go 內部在型別轉換時到底做了哪些工作。通過這個例子,我會解釋結點樹是如何生成並被使用的。同樣地,你也可以將這篇部落格的知識應用到其它 Go 編譯器特徵的研究中。

前言

要完成這個實驗,我們需要直接使用 Go 編譯器(而不是使用 Go 工具)。你可以通過下面的命令來使用:

Shell
1 go tool6gtest.go

這個命令會編譯 test.go 原始檔並生成目標檔案。這裡, 6g 是 AMD64 架構上編譯器的名稱。請注意,如果你在不同的架構上,請使用不同的編譯器。

在直接使用編譯器的時候,我們可能會用到一些命令列引數(詳細內容請參考

這裡)。在這個實驗中,我們會用到 -W 引數,這個引數會輸出結點樹的佈局結構。

建立一個簡單的 Go 程式

首先,我們需要先編寫一個簡單的 Go 程式。 我編寫的程式如下:

C
123456789101112131415161718 packagemaintypeIinterface{DoSomeWork()}typeTstruct{aint}func(t*T)DoSomeWork(){}func main(){t:=&T{}i:=I(t)print(i)}

這段程式碼非常簡單,不是嗎?其中第 17 輸出了變數 i 的值,這一行程式碼看上去多此一舉。但是,如果沒有這一行程式碼,程式中就沒有使用到變數 i,那麼整個程式就不會被編譯。接下來,我們將使用 -W 引數來編譯我們的程式:

Shell
1 go tool6g-Wtest.go

完成編譯後,你會看到輸出中包含了程式中定義的每個方法的結點樹。在我們這個例子中有 main 和 init 方法。init 方法是隱式生成的,所有的程式都會有這個方法。此處,我們暫將該方法擱置在一邊。

對於每個方法,編譯器都會輸出兩個版本的結點樹。第一個是剛解析完原始檔生成的原始結點樹。另外一個則是完成型別檢查以及一些必須的修改後的結點樹。

分析 main 方法的結點樹

讓我們仔細看一下 main 方法的最初版本結點樹,儘量搞清楚 Go 編譯器到底做了哪些工作。

123456789101112131415161718192021222324252627 DCLl(15).NAME-main.tu(1)a(1)g(1)l(15)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)PTR64-*main.TASl(15)colas(1)tc(1).NAME-main.tu(1)a(1)g(1)l(15)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)PTR64-*main.T.PTRLITl(15)esc(no)ld(1)tc(1)PTR64-*main.T..STRUCTLITl(15)tc(1)main.T...TYPE<S>l(15)tc(1)implicit(1)type=PTR64-*main.TPTR64-*main.TDCLl(16).NAME-main.iu(1)a(1)g(2)l(16)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)main.IASl(16)tc(1).NAME-main.autotmp_0000u(1)a(1)l(16)x(0+0)class(PAUTO)esc(N)tc(1)used(1)PTR64-*main.T.NAME-main.tu(1)a(1)g(1)l(15)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)PTR64-*main.TASl(16)colas(1)tc(1).NAME-main.iu(1)a(1)g(2)l(16)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)main.I.CONVIFACEl(16)tc(1)main.I..NAME-main.autotmp_0000u(1)a(1)l(16)x(0+0)class(PAUTO)esc(N)tc(1)used(1)PTR64-*main.TVARKILLl(16)tc(1).NAME-main.autotmp_0000u(1)a(1)l(16)x(0+0)class(PAUTO)esc(N)tc(1)used(1)PTR64-*main.TPRINTl(17)tc(1)PRINT-list.NAME-main.iu(1)a(1)g(2)l(16)x(0+0)class(PAUTO)f(1)ld(1)tc(1)used(1)main.I

下面的分析過程中,我會刪除結點樹中一些不必要的資訊。

第一個結點非常的簡單:

C++
12 DCLl(15).NAME-main.tl(15)PTR64-*main.T

第一個結點是一個宣告結點。 l(15) 說明這個結點的定義在原始碼的第 15 行。這個宣告結點引用了表示 main.t 變數的名稱結點。這個變數是定義在 main 包中指向 main.T 型別的一個 64 位指標。你去看一下原始碼中的第 15 行就很容易就明白這個宣告代表著什麼了。

接下來這個結點又是一個宣告結點。這一次,這個宣告結點聲明瞭一個屬於 main.T 型別的變數 main.i。

C++
12 DCLl(16).NAME-main.il(16)main.I

然後,編譯器建立了另外一個變數 autotmp_0000, 並將變數 main.t 賦值給該變數。

C++
123 ASl(16)tc(1).NAME-main.autotmp_0000l(16)PTR64-*main.T.NAME-main.tl(15)PTR64-*main.T

最後,我們終於看到我們真正感興趣的結點。

C++
1234 ASl(16).NAME-main.il(16)main.I.CONVIFACEl(16)main.I..NAME-main.autotmp_0000 PTR64-*main.T

我們可以看到編譯器將一個特殊的結點 CONVIFACE 賦值給了變數 main.i。但是,這並沒有告訴我們在這個賦值背後到底發生了什麼。為了搞清楚幕後真相,我們需要去分析一下修改完成後的 main 方法結點樹(你可以在輸出資訊的 “after walk main” 這一小節中看到相關的資訊)。

編譯器怎麼翻譯賦值結點

下面,你將看到編譯器到底是如何翻譯賦值結點的:

C++
1234567891011121314151617181920212223242526272829303132333435363738 AS-init.ASl(16)..NAME-main.autotmp_0003l(16)PTR64-*uint8..NAME-go.itab.*"".T."".Il(16)PTR64-*uint8.IFl(16).IF-test..EQl(16)bool...NAME-main.autotmp_0003l(16)PTR64-*uint8...LITERAL-nilI(16)PTR64-*uint8.IF-body..ASl(16)...NAME-main.autotmp_0003l(16)PTR64-*uint8...CALLFUNCl(16)PTR64-*byte....NAME-runtime.typ2Itabl(2)FUNC-funcSTRUCT-(FIELD-.....NAME-runtime.typ·2l(2)PTR64-*byte,FIELD-.....NAME-runtime.typ2·3l(2)PTR64-*bytePTR64-*byte,FIELD-.....NAME-runtime.cache·4<