Go程序如何编译为机器代码
省略原文一些广告
今天,我们将看看Go 1.11的编译器,以及它如何将Go源代码编译成可执行文件,以便了解我们使用的工具是如何工作的。 我们还将看到为什么Go代码如此之快,以及编译器对此的作用。 我们将看一下编译器的三个阶段:
- 扫描程序,将源码转换为token(标记),供解析器使用。
- 解析器,将token转换为抽象语法树,供代码生成使用。
- 生成代码,将抽象语法树转换为机器代码。
注意:我们将要使用的包( go / scanner , go / parser , go / token , go / ast 等)不是Go编译器真正使用的。 但是,真实的Go编译器具有非常相似的结构和语义。
Scanner/扫描器
每个编译器的第一步是将原始源码文本分解为token,这是由扫描程序(也称为词法分析器)完成的。 标记可以是关键字,字符串,变量名,函数名等。每个有效的程序“单词”由token表示。 对于Go来说,这可能意味着我们“token”了“package”,“main”,“func”等等。
每个token由其在源码中的位置,类型和原始文本表示。 Go甚至允许我们使用go / scanner和go / token包在Go程序中自己执行扫描程序。 这意味着我们可以在扫描完成后检查Go程序的程序。 为此,我们将创建一个打印Hello World程序的所有标记的简单程序。
该程序将如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package main
import ( "fmt" "go/scanner" "go/token" )
func main() { src := []byte(`package main import "fmt" func main() { fmt.Println("Hello, world!") } `)
var s scanner.Scanner fset := token.NewFileSet() file := fset.AddFile("", fset.Base(), len(src)) s.Init(file, src, nil, 0)
for { pos, tok, lit := s.Scan() fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit)
if tok == token.EOF { break } } }
|
我们将创建源码字符串,并初始化scan.Scanner结构,扫描我们的源代码。 我们尽可能多地调用Scan()并打印token的位置,类型和文字,直到我们到达文件结束( EOF )标记。
当我们运行程序时,它将打印以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 1:1 package "package" 1:9 IDENT "main" 1:13 ; "\n" 2:1 import "import" 2:8 STRING "\"fmt\"" 2:13 ; "\n" 3:1 func "func" 3:6 IDENT "main" 3:10 ( "" 3:11 ) "" 3:13 { "" 4:3 IDENT "fmt" 4:6 . "" 4:7 IDENT "Println" 4:14 ( "" 4:15 STRING "\"Hello, world!\"" 4:30 ) "" 4:31 ; "\n" 5:1 } "" 5:2 ; "\n" 5:3 EOF ""
|
在这里,我们可以看到Go解析器在编译程序时所使用的内容。 我们还可以看到扫描器添加了分号,用于将其放置在其他编程语言(如C)中。这解释了为什么Go不需要分号:它们由扫描器智能放置。
Parser/分析器
扫描源码后,它将被传递至解析器。 解析器是编译器的一个阶段,它将标记转换为抽象语法树(AST)。 AST是源码的结构化表示。 在AST中,我们将能够看到程序结构,例如函数和常量声明。
Go再次为我们提供了解析程序的包并查看AST: go / parser和go / ast 。 我们可以像这样使用它们来打印完整的AST:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package main
import ( "go/ast" "go/parser" "go/token" "log" )
func main() { src := []byte(`package main import "fmt" func main() { fmt.Println("Hello, world!") } `)
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", src, 0) if err != nil { log.Fatal(err) }
ast.Print(fset, file) }
|
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| 0 *ast.File { 1 . Package: 1:1 2 . Name: *ast.Ident { 3 . . NamePos: 1:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: 3:1 9 . . . Tok: import 10 . . . Lparen: - 11 . . . Specs: []ast.Spec (len = 1) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: 3:8 15 . . . . . . Kind: STRING 16 . . . . . . Value: "\"fmt\"" 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . } 21 . . . Rparen: - 22 . . } 23 . . 1: *ast.FuncDecl { 24 . . . Name: *ast.Ident { 25 . . . . NamePos: 5:6 26 . . . . Name: "main" 27 . . . . Obj: *ast.Object { 28 . . . . . Kind: func 29 . . . . . Name: "main" 30 . . . . . Decl: *(obj @ 23) 31 . . . . } 32 . . . } 33 . . . Type: *ast.FuncType { 34 . . . . Func: 5:1 35 . . . . Params: *ast.FieldList { 36 . . . . . Opening: 5:10 37 . . . . . Closing: 5:11 38 . . . . } 39 . . . } 40 . . . Body: *ast.BlockStmt { 41 . . . . Lbrace: 5:13 42 . . . . List: []ast.Stmt (len = 1) { 43 . . . . . 0: *ast.ExprStmt { 44 . . . . . . X: *ast.CallExpr { 45 . . . . . . . Fun: *ast.SelectorExpr { 46 . . . . . . . . X: *ast.Ident { 47 . . . . . . . . . NamePos: 6:2 48 . . . . . . . . . Name: "fmt" 49 . . . . . . . . } 50 . . . . . . . . Sel: *ast.Ident { 51 . . . . . . . . . NamePos: 6:6 52 . . . . . . . . . Name: "Println" 53 . . . . . . . . } 54 . . . . . . . } 55 . . . . . . . Lparen: 6:13 56 . . . . . . . Args: []ast.Expr (len = 1) { 57 . . . . . . . . 0: *ast.BasicLit { 58 . . . . . . . . . ValuePos: 6:14 59 . . . . . . . . . Kind: STRING 60 . . . . . . . . . Value: "\"Hello, world!\"" 61 . . . . . . . . } 62 . . . . . . . } 63 . . . . . . . Ellipsis: - 64 . . . . . . . Rparen: 6:29 65 . . . . . . } 66 . . . . . } 67 . . . . } 68 . . . . Rbrace: 7:1 69 . . . } 70 . . } 71 . } .. . .. // Left out for brevity 83 }
|
在输出中,可以看到有关该程序的一些信息。 在Decls字段中,有一个文件中所有声明的列表,例如导入,常量,变量和函数。 在本例子中,只有两个声明:fmt包和main函数。
为了进一步消化它,我们可以看一下这个图,它是上述数据的表示,但只包括类型,红色代表与节点对应的代码:

主要功能由三部分组成:名称,声明和正文。 名称表示main的值的标识符。 声明由Type字段指定,并包含参数列表和返回类型(如果我们指定了参数列表和返回类型)。 正文包含程序所有行的语句列表,在本例子中,只有一行。
单个fmt.Println语句由AST中的一部分组成。 该语句是一个ExprStmt ,表示一个表达式,例如,它可以是一个函数调用,就像本例子一样,或者它可以是一个文字,一个二进制操作(例如加法和减法),一个一元操作(用于实例否定一个数字)等等。任何能在函数中调用并使用的东西都可以是表达式。
我们的ExprStmt包含一个CallExpr ,它是我们实际的函数调用。 包括几个部分,其中最重要的部分是Fun和Args 。 Fun包含对函数调用的引用,在这种情况下,它是一个SelectorExpr ,因为我们从fmt包中选择Println标识符。 但是,在AST中,编译器还不知道fmt是一个包,它也可能是AST中的一个变量。
Args包含表达式列表,这些表达式是函数的参数。 在这种情况下,我们将一个文字字符串传递给函数,因此它由一个类型为STRING的BasicLit表示。
很明显,我们能够从AST中推断出很多。 这意味着我们还可以进一步检查AST并查找文件中的所有函数调用。 为此,我们将使用ast包中的Inspect函数。 此函数将递归遍历树,并允许我们检查来自所有节点的信息。
要提取所有函数调用,我们将使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package main
import ( "fmt" "go/ast" "go/parser" "go/printer" "go/token" "log" "os" )
func main() { src := []byte(`package main import "fmt" func main() { fmt.Println("Hello, world!") } `)
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", src, 0) if err != nil { log.Fatal(err) }
ast.Inspect(file, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true }
printer.Fprint(os.Stdout, fset, call.Fun) fmt.Println()
return false }) }
|
我们在这里做的是查找所有节点以及它们是否为* ast.CallExpr类型,我们刚才看到它代表了我们的函数调用。 如果是,我们将使用fmt包打印Fun成员中存在的函数的名称。
此代码的输出将是:
fmt.Println
这确实是我们简单程序中唯一的函数调用,所以我们找到了所有函数调用。
构建AST后,将使用GOPATH或Go 1.11及更高版本的模块将所有导入解析。 然后,将检查类型,并应用一些初步优化,使程序执行得更快。
Code generation/生成代码
//TODO