go語言需要編譯。 Go語言是編譯型的靜態語言,是一門需要編譯才能運行的程式語言,也就說Go語言程式在運行之前需要透過編譯器產生二進位機器碼(二進位的可執行檔),隨後二進位檔才能在目標機器上運作。
本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。
Go語言是一門需要編譯才能運行的程式語言,也就說程式碼在運作之前需要透過編譯器產生二進位機器碼,隨後二進位檔案才能在目標機器上運作。
簡單來說,Go語言是編譯型的靜態語言(和C語言一樣),所以在執行Go語言程式之前,先將其編譯成二進位的可執行檔。
如果我們想要了解Go語言的實作原理,理解它的編譯過程就是一個沒有辦法繞過的事情。下面就來看看Go語言是怎麼完成編譯的。
想要深入了解Go語言的編譯過程,需要事先了解編譯過程中所涉及的一些術語和專業知識。這些知識其實在我們的日常工作和學習中比較難用到,但是對於理解編譯的過程和原理還是非常重要的。
在電腦科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree) ,是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。
之所以說文法是「抽象」的,是因為這裡的文法並不會表示出真實文法中出現的每個細節。例如,嵌套括號被隱含在樹的結構中,並沒有以節點的形式呈現。而類似於 if else 這樣的條件判斷語句,可以使用兩個分支的節點來表示。
以算術表達式1 3*(4-1) 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 的特性能夠為整個程式實現以下的最佳化:
暫存器分配(register allocation)
因為 SSA 的作用的主要作用是程式碼的最佳化,所以是編譯器後端(主要負責目標程式碼的最佳化和產生)的一部分。當然,除了 SSA 之外程式碼編譯領域還有非常多的中間程式碼最佳化方法,而最佳化編譯器產生的程式碼是一個非常古老且複雜的領域,這裡就不展開介紹了。
最後要介紹的一個預備知識就是指令集架構了,指令集架構(Instruction Set Architecture,簡稱ISA) ,又稱指令集或指令集體系,是電腦體系結構中與程式設計有關的部分,包含了基本資料類型,指令集,暫存器,尋址模式,儲存體系,中斷,異常處理以及外部I/O。指令集架構包含一系列的 opcode 即操作碼(機器語言),以及由特定處理器執行的基本指令。
指令集架構常見種類如下:
不同的處理器(CPU)使用了大不相同的機器語言,所以我們的程式想要在不同的機器上運行,就需要將原始碼根據架構編譯成不同的機器語言。
Go語言編譯器的原始碼在cmd/compile 目錄中,目錄下的檔案共同構成了Go語言的編譯器,學過編譯原理的人可能聽說過編譯器的前端和後端,編譯器的前端一般承擔著詞法分析、語法分析、類型檢查和中間代碼生成幾部分工作,而編譯器後端主要負責目標代碼的生成和最佳化,也就是將中間程式碼翻譯成目標機器能夠運作的機器碼。
Go的編譯器在邏輯上可以分成四個階段:詞法與語法分析、類型檢查和AST 轉換、通用SSA 生成和最後的機器碼生成,下面我們來分別介紹這四個階段所做的工作。
所有的編譯過程其實都是從解析程式碼的原始檔開始的,詞法分析的作用就是解析原始碼程式碼文件,它將文件中的字串序列轉換成Token 序列,方便後面的處理和解析,我們一般會把執行詞法分析的程序稱為詞法解析器(lexer)。
而語法分析的輸入就是詞法分析器輸出的Token 序列,這些序列會依照順序被語法分析器解析,語法的解析過程就是將詞法分析產生的Token 依照語言定義好的文法( Grammar)自下而上或自上而下的進行規約,每一個Go 的源碼檔案最終會被歸納成一個SourceFile 結構:
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的Go语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。
如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。
当拿到一组文件的抽象语法树 AST 之后,Go语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:
外部的声明;
通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。
类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。
其实类型检查不止对类型进行了验证工作,还对 AST 进行了改写以及处理Go语言内置的关键字,所以,这一过程在整个编译流程中是非常重要的,没有这个步骤很多关键字其实就没有办法工作。【相关推荐:Go视频教程】
当我们将源文件转换成了抽象语法树,对整个语法树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go语言的编译器就会将输入的 AST 转换成中间代码。
Go语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。
在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个Go语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。
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語言是否需要編譯的詳細內容。更多資訊請關注PHP中文網其他相關文章!