今天我们一起来研究 Go 1.11 的编译器,以及它将 Go 程序代码编译成可执行文件的过程。以便了解我们日常使用的工具是如何工作的。
本文还会带你了解 Go 程序为什么这么快,以及编译器在这中间起到了什么作用。

首先,编译器的三个阶段:

tokenparserparsertokenAST(抽象语法树)AST

注意:下面使用的代码包(go/scannergo/parsergo/tokengo/ast)主要是让我们可以方便地对 Go 代码进行解析和生成,做出更有趣的事情。但是 Go 本身的编译器并不是用这些代码包实现的。

扫描代码,进行词法分析

tokentokentoken"package""main""func"token
tokentoken
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
        }
    }
}
tokenscan.ScannerScan()token

输出:

2:1   package "package"
2:9   IDENT   "main"
2:13  ;       "\n"
4:1   import  "import"
4:8   STRING  "\"fmt\""
4:13  ;       "\n"
6:1   func    "func"
6:6   IDENT   "main"
6:10  (       ""
6:11  )       ""
6:13  {       ""
7:2   IDENT   "fmt"
7:5   .       ""
7:6   IDENT   "Println"
7:13  (       ""
7:14  STRING  "\"Hello, world!\""
7:29  )       ""
7:30  ;       "\n"
8:1   }       ""
8:2   ;       "\n"
8:3   EOF     ""
2:1packagetokenpackage"package"Scanner\n;Scanner

语法分析

token抽象语法树(AST)ASTAST
AST
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)
}

输出:

     0  *ast.File {
     1  .  Package: 2:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: 2:9
     4  .  .  Name: "main"
     5  .  }
     6  .  Decls: []ast.Decl (len = 2) {
     7  .  .  0: *ast.GenDecl {
     8  .  .  .  TokPos: 4:1
     9  .  .  .  Tok: import
    10  .  .  .  Lparen: -
    11  .  .  .  Specs: []ast.Spec (len = 1) {
    12  .  .  .  .  0: *ast.ImportSpec {
    13  .  .  .  .  .  Path: *ast.BasicLit {
    14  .  .  .  .  .  .  ValuePos: 4: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: 6: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: 6:1
    35  .  .  .  .  Params: *ast.FieldList {
    36  .  .  .  .  .  Opening: 6:10
    37  .  .  .  .  .  Closing: 6:11
    38  .  .  .  .  }
    39  .  .  .  }
    40  .  .  .  Body: *ast.BlockStmt {
    41  .  .  .  .  Lbrace: 6: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: 7:2
    48  .  .  .  .  .  .  .  .  .  Name: "fmt"
    49  .  .  .  .  .  .  .  .  }
    50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    51  .  .  .  .  .  .  .  .  .  NamePos: 7:6
    52  .  .  .  .  .  .  .  .  .  Name: "Println"
    53  .  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  .  }
    55  .  .  .  .  .  .  .  Lparen: 7:13
    56  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    58  .  .  .  .  .  .  .  .  .  ValuePos: 7:14
    59  .  .  .  .  .  .  .  .  .  Kind: STRING
    60  .  .  .  .  .  .  .  .  .  Value: "\"Hello, world!\""
    61  .  .  .  .  .  .  .  .  }
    62  .  .  .  .  .  .  .  }
    63  .  .  .  .  .  .  .  Ellipsis: -
    64  .  .  .  .  .  .  .  Rparen: 7:29
    65  .  .  .  .  .  .  }
    66  .  .  .  .  .  }
    67  .  .  .  .  }
    68  .  .  .  .  Rbrace: 8:1
    69  .  .  .  }
    70  .  .  }
    71  .  }
    72  .  Scope: *ast.Scope {
    73  .  .  Objects: map[string]*ast.Object (len = 1) {
    74  .  .  .  "main": *(obj @ 27)
    75  .  .  }
    76  .  }
    77  .  Imports: []*ast.ImportSpec (len = 1) {
    78  .  .  0: *(obj @ 12)
    79  .  }
    80  .  Unresolved: []*ast.Ident (len = 1) {
    81  .  .  0: *(obj @ 46)
    82  .  }
    83  }
Decls
main函数Typefmt.Println("Hello, world!")
fmt.PrintlnASTExprStmt
ExprStmtCallExprFunArgsFunSelectorExprfmtPrintlnASTfmtAST
ArgsSTRINGBasicLit
ASTASTInspect
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/printer"
    "go/token"
    "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 {
        fmt.Println(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)
        
        return false
    })
}

输出:

fmt.Println
*ast.CallExprFun
ASTGOPATH

代码生成

AST
ASTSSA
SSA
SSA
package main

import "fmt"

func main() {
    fmt.Println(2)
}

如你所见,此程序只有一个函数和一个导入。它会在运行时打印 2。但是,此例足以让我们了解SSA。

SSAGOSSAFUNCmain
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags -S main.go
SSAssa.html
ssa.htmlstartASTSSAlowerSSASSAgenssa
start
b1:
    v1  = InitMem <mem>
    v2  = SP <uintptr>
    v3  = SB <uintptr>
    v4  = ConstInterface <interface {}>
    v5  = ArrayMake1 <[1]interface {}> v4
    v6  = VarDef <mem> {.autotmp_0} v1
    v7  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v6
    v8  = Store <mem> {[1]interface {}} v7 v5 v6
    v9  = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v8
    v10 = Addr <*uint8> {type.int} v3
    v11 = Addr <*int> {"".statictmp_0} v3
    v12 = IMake <interface {}> v10 v11
    v13 = NilCheck <void> v9 v8
    v14 = Const64 <int> [0]
    v15 = Const64 <int> [1]
    v16 = PtrIndex <*interface {}> v9 v14
    v17 = Store <mem> {interface {}} v16 v12 v8
    v18 = NilCheck <void> v9 v17
    v19 = IsSliceInBounds <bool> v14 v15
    v24 = OffPtr <*[]interface {}> [0] v2
    v28 = OffPtr <*int> [24] v2
If v19 → b2 b3 (likely) (line 6)

b2: ← b1
    v22 = Sub64 <int> v15 v14
    v23 = SliceMake <[]interface {}> v9 v22 v22
    v25 = Copy <mem> v17
    v26 = Store <mem> {[]interface {}} v24 v23 v25
    v27 = StaticCall <mem> {fmt.Println} [48] v26
    v29 = VarKill <mem> {.autotmp_0} v27
Ret v29 (line 7)

b3: ← b1
    v20 = Copy <mem> v17
    v21 = StaticCall <mem> {runtime.panicslice} v20
Exit v21 (line 6)
SSA
vb2b3b1If v19 → b2 b3 (likely)v19IsSliceInBounds  v14 v15IsSliceInBounds0 <= arg0 <= arg1v14v15v14 = Const64  [0]0 <= 0 <= 1
optv19ConstBool  [true]opt deadcodeb3b3
SSA机器特定的SSAlowerlowered deadcodelower
b1:
    BlockInvalid (6)
b2:
    v2 (?) = SP <uintptr>
    v3 (?) = SB <uintptr>
    v10 (?) = LEAQ <*uint8> {type.int} v3
    v11 (?) = LEAQ <*int> {"".statictmp_0} v3
    v15 (?) = MOVQconst <int> [1]
    v20 (?) = MOVQconst <uintptr> [0]
    v25 (?) = MOVQconst <*uint8> [0]
    v1 (?) = InitMem <mem>
    v6 (6) = VarDef <mem> {.autotmp_0} v1
    v7 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2
    v9 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2
    v16 (+6) = LEAQ <*interface {}> {.autotmp_0} v2
    v18 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2
    v21 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2
    v30 (6) = LEAQ <*int> [16] v2
    v19 (6) = LEAQ <*int> [8] v2
    v23 (6) = MOVOconst <int128> [0]
    v8 (6) = MOVOstore <mem> {.autotmp_0} v2 v23 v6
    v22 (6) = MOVQstore <mem> {.autotmp_0} v2 v10 v8
    v17 (6) = MOVQstore <mem> {.autotmp_0} [8] v2 v11 v22
    v14 (6) = MOVQstore <mem> v2 v9 v17
    v28 (6) = MOVQstoreconst <mem> [val=1,off=8] v2 v14
    v26 (6) = MOVQstoreconst <mem> [val=1,off=16] v2 v28
    v27 (6) = CALLstatic <mem> {fmt.Println} [48] v26
    v29 (5) = VarKill <mem> {.autotmp_0} v27
Ret v29 (+7)
v15 (?) = MOVQconst  [1]v15MOVQconstConst64v15v15
ASTSSA
SSA

结论

Go 是一种非常高效且高性能的语言,由其编译器及其优化支撑。要了解有关 Go 编译器的更多信息,源代码的 README 是不错的选择。