go語言是否需要編譯

2022-12-01 22:01:28

go語言需要編譯。Go語言是編譯型的靜態語言,是一門需要編譯才能執行的程式語言,也就說Go語言程式在執行之前需要通過編譯器生成二進位制機器碼(二進位制的可執行檔案),隨後二進位制檔案才能在目標機器上執行。

php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

Go語言是一門需要編譯才能執行的程式語言,也就說程式碼在執行之前需要通過編譯器生成二進位制機器碼,隨後二進位制檔案才能在目標機器上執行。

簡單來說,Go語言是編譯型的靜態語言(和C語言一樣),所以在執行Go語言程式之前,先要將其編譯成二進位制的可執行檔案。

如果我們想要了解Go語言的實現原理,理解它的編譯過程就是一個沒有辦法繞過的事情。下面就來看看Go語言是怎麼完成編譯的。

預備知識

想要深入瞭解Go語言的編譯過程,需要提前瞭解一下編譯過程中涉及的一些術語和專業知識。這些知識其實在我們的日常工作和學習中比較難用到,但是對於理解編譯的過程和原理還是非常重要的。

1) 抽象語法樹

在電腦科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。

之所以說語法是「抽象」的,是因為這裡的語法並不會表示出真實語法中出現的每個細節。比如,巢狀括號被隱含在樹的結構中,並沒有以節點的形式呈現。而類似於 if else 這樣的條件判斷語句,可以使用帶有兩個分支的節點來表示。

以算術表示式 1+3*(4-1)+2 為例,可以解析出的抽象語法樹如下圖所示:

1.gif

圖:抽象語法樹

抽象語法樹可以應用在很多領域,比如瀏覽器,智慧編輯器,編譯器。

2) 靜態單賦值

在編譯器設計中,靜態單賦值形式(static single assignment form,通常簡寫為 SSA form 或是 SSA)是中介碼(IR,intermediate representation)的屬性,它要求每個變數只分配一次,並且變數需要在使用之前定義。在實踐中我們通常會用新增下標的方式實現每個變數只能被賦值一次的特性,這裡以下面的程式碼舉一個簡單的例子:

x := 1
x := 2
y := x
登入後複製

從上面的描述所知,第一行賦值行為是不需要的,因為 x 在第二行被二度賦值並在第三行被使用,在 SSA 下,將會變成下列的形式:

x1 := 1
x2 := 2
y1 := x2
登入後複製

從使用 SSA 的中間程式碼我們就可以非常清晰地看出變數 y1 的值和 x1 是完全沒有任何關係的,所以在機器碼生成時其實就可以省略第一步,這樣就能減少需要執行的指令來優化這一段程式碼。

根據 Wikipedia(維基百科)對 SSA 的介紹來看,在中間程式碼中使用 SSA 的特效能夠為整個程式實現以下的優化:

  • 常數傳播(constant propagation)
  • 值域傳播(value range propagation)
  • 稀疏有條件的常數傳播(sparse conditional constant propagation)
  • 消除無用的程式碼(dead code elimination)
  • 全域數值編號(global value numbering)
  • 消除部分的冗餘(partial redundancy elimination)
  • 強度折減(strength reduction)
  • 暫存器分配(register allocation)

因為 SSA 的作用的主要作用就是程式碼的優化,所以是編譯器後端(主要負責目的碼的優化和生成)的一部分。當然,除了 SSA 之外程式碼編譯領域還有非常多的中間程式碼優化方法,優化編譯器生成的程式碼是一個非常古老並且複雜的領域,這裡就不展開介紹了。

3) 指令集架構

最後要介紹的一個預備知識就是指令集架構了,指令集架構(Instruction Set Architecture,簡稱 ISA),又稱指令集或指令集體系,是電腦架構中與程式設計有關的部分,包含了基本資料型別,指令集,暫存器,定址模式,儲存體系,中斷,例外處理以及外部 I/O。指令集架構包含一系列的 opcode 即操作碼(機器語言),以及由特定處理器執行的基本命令。

指令集架構常見種類如下:

  • 複雜指令集運算(Complex Instruction Set Computing,簡稱 CISC);
  • 精簡指令集運算(Reduced Instruction Set Computing,簡稱 RISC);
  • 顯式並行指令集運算(Explicitly Parallel Instruction Computing,簡稱 EPIC);
  • 超長指令字指令集運算(VLIW)。


不同的處理器(CPU)使用了大不相同的機器語言,所以我們的程式想要在不同的機器上執行,就需要將原始碼根據架構編譯成不同的機器語言。

編譯原理

Go語言編譯器的原始碼在 cmd/compile 目錄中,目錄下的檔案共同構成了Go語言的編譯器,學過編譯原理的人可能聽說過編譯器的前端和後端,編譯器的前端一般承擔著詞法分析、語法分析、型別檢查和中間程式碼生成幾部分工作,而編譯器後端主要負責目的碼的生成和優化,也就是將中間程式碼翻譯成目標機器能夠執行的機器碼。

2.gif

Go的編譯器在邏輯上可以被分成四個階段:詞法與語法分析、型別檢查和 AST 轉換、通用 SSA 生成和最後的機器程式碼生成,下面我們來分別介紹一下這四個階段做的工作。

1) 詞法與語法分析

所有的編譯過程其實都是從解析程式碼的原始檔開始的,詞法分析的作用就是解析原始碼檔案,它將檔案中的字串序列轉換成 Token 序列,方便後面的處理和解析,我們一般會把執行詞法分析的程式稱為詞法解析器(lexer)。

而語法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會按照順序被語法分析器進行解析,語法的解析過程就是將詞法分析生成的 Token 按照語言定義好的文法(Grammar)自下而上或者自上而下的進行規約,每一個 Go 的原始碼檔案最終會被歸納成一個 SourceFile 結構:

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
登入後複製

標準的 Golang 語法解析器使用的就是 LALR(1) 的文法,語法解析的結果其實就是上面介紹過的抽象語法樹(AST),每一個 AST 都對應著一個單獨的Go語言檔案,這個抽象語法樹中包括當前檔案屬於的包名、定義的常數、結構體和函數等。

如果在語法解析的過程中發生了任何語法錯誤,都會被語法解析器發現並將訊息列印到標準輸出上,整個編譯過程也會隨著錯誤的出現而被中止。

2) 型別檢查

當拿到一組檔案的抽象語法樹 AST 之後,Go語言的編譯器會對語法樹中定義和使用的型別進行檢查,型別檢查分別會按照順序對不同型別的節點進行驗證,按照以下的順序進行處理:

  • 常數、型別和函數名及型別;
  • 變數的賦值和初始化;
  • 函數和閉包的主體;
  • 雜湊鍵值對的型別;
  • 匯入函數體;
  • 外部的宣告;

通過對每一棵抽象節點樹的遍歷,我們在每一個節點上都會對當前子樹的型別進行驗證保證當前節點上不會出現型別錯誤的問題,所有的型別錯誤和不匹配都會在這一個階段被發現和暴露出來。

型別檢查的階段不止會對樹狀結構的節點進行驗證,同時也會對一些內建的函數進行展開和改寫,例如 make 關鍵字在這個階段會根據子樹的結構被替換成 makeslice 或者 makechan 等函數。

其實型別檢查不止對型別進行了驗證工作,還對 AST 進行了改寫以及處理Go語言內建的關鍵字,所以,這一過程在整個編譯流程中是非常重要的,沒有這個步驟很多關鍵字其實就沒有辦法工作。【相關推薦:Go視訊教學

3) 中間程式碼生成

當我們將原始檔轉換成了抽象語法樹,對整個語法樹的語法進行解析並進行型別檢查之後,就可以認為當前檔案中的程式碼基本上不存在無法編譯或者語法錯誤的問題了,Go語言的編譯器就會將輸入的 AST 轉換成中間程式碼。

Go語言編譯器的中間程式碼使用了 SSA(Static Single Assignment Form) 的特性,如果我們在中間程式碼生成的過程中使用這種特性,就能夠比較容易的分析出程式碼中的無用變數和片段並對程式碼進行優化。

在型別檢查之後,就會通過一個名為 compileFunctions 的函數開始對整個Go語言專案中的全部函數進行編譯,這些函數會在一個編譯佇列中等待幾個後端工作協程的消費,這些 Goroutine 會將所有函數對應的 AST 轉換成使用 SSA 特性的中間程式碼。

4) 機器碼生成

Go語言原始碼的 cmd/compile/internal 目錄中包含了非常多機器碼生成相關的包,不同型別的 CPU 分別使用了不同的包進行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是說Go語言能夠在幾乎全部常見的 CPU 指令集型別上執行。

編譯器入口

Go語言的編譯器入口是 src/cmd/compile/internal/gc 包中的 main.go 檔案,這個 600 多行的 Main 函數就是Go語言編譯器的主程式,這個函數會先獲取命令列傳入的引數並更新編譯的選項和設定,隨後就會開始執行 parseFiles 函數對輸入的所有檔案進行詞法與語法分析得到檔案對應的抽象語法樹:

func Main(archInit func(*Arch)) {
    // ...
    lines := parseFiles(flag.Args())
登入後複製

接下來就會分九個階段對抽象語法樹進行更新和編譯,就像我們在上面介紹的,整個過程會經歷型別檢查、SSA 中間程式碼生成以及機器碼生成三個部分:

  • 檢查常數、型別和函數的型別;
  • 處理變數的賦值;
  • 對函數的主體進行型別檢查;
  • 決定如何捕獲變數;
  • 檢查行內函式的型別;
  • 進行逃逸分析;
  • 將閉包的主體轉換成參照的捕獲變數;
  • 編譯頂層函數;
  • 檢查外部依賴的宣告;

瞭解了剩下的編譯過程之後,我們重新回到詞法和語法分析後的具體流程,在這裡編譯器會對生成語法樹中的節點執行型別檢查,除了常數、型別和函數這些頂層宣告之外,它還會對變數的賦值語句、函數主體等結構進行檢查:

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) {
        xtop[i] = typecheck(n, ctxStmt)
    }
}

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias {
        xtop[i] = typecheck(n, ctxStmt)
    }
}

for i := 0; i < len(xtop); i++ {
    n := xtop[i]
    if op := n.Op; op == ODCLFUNC || op == OCLOSURE {
        typecheckslice(Curfn.Nbody.Slice(), ctxStmt)
    }
}

checkMapKeys()

for _, n := range xtop {
    if n.Op == ODCLFUNC && n.Func.Closure != nil {
        capturevars(n)
    }
}

escapes(xtop)

for _, n := range xtop {
    if n.Op == ODCLFUNC && n.Func.Closure != nil {
        transformclosure(n)
    }
}
登入後複製

型別檢查會對傳入節點的子節點進行遍歷,這個過程會對 make 等關鍵字進行展開和重寫,型別檢查結束之後並沒有輸出新的資料結構,只是改變了語法樹中的一些節點,同時這個過程的結束也意味著原始碼中已經不存在語法錯誤和型別錯誤,中間程式碼和機器碼也都可以正常的生成了。

    initssaconfig()

    peekitabs()

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if n.Op == ODCLFUNC {
            funccompile(n)
        }
    }

    compileFunctions()

    for i, n := range externdcl {
        if n.Op == ONAME {
            externdcl[i] = typecheck(externdcl[i], ctxExpr)
        }
    }

    checkMapKeys()
}
登入後複製

在主程式執行的最後,會將頂層的函數編譯成中間程式碼並根據目標的 CPU 架構生成機器碼,不過這裡其實也可能會再次對外部依賴進行型別檢查以驗證正確性。

總結

Go語言的編譯過程其實是非常有趣並且值得學習的,通過對Go語言四個編譯階段的分析和對編譯器主函數的梳理,我們能夠對 Golang 的實現有一些基本的理解,掌握編譯的過程之後,Go語言對於我們來講也不再那麼神祕,所以學習其編譯原理的過程還是非常有必要的。

更多程式設計相關知識,請存取:!!

以上就是go語言是否需要編譯的詳細內容,更多請關注TW511.COM其它相關文章!