Does go language need to be compiled?

青灯夜游
Release: 2022-12-01 19:06:35
Original
7117 people have browsed it

Go language needs to be compiled. Go language is a compiled static language. It is a programming language that needs to be compiled to run. In other words, before running a Go language program, a compiler needs to generate binary machine code (binary executable file), and then the binary file can be run on the target machine.

Does go language need to be compiled?

The operating environment of this tutorial: Windows 7 system, GO version 1.18, Dell G3 computer.

Go language is a programming language that requires compilation to run, which means that the code needs to be generated by a compiler before running, and then the binary file can be run on the target machine.

Simply put, Go language is a compiled static language (the same as C language), so before running a Go language program, it must be compiled into a binary executable file.

If we want to understand the implementation principles of Go language, understanding its compilation process is something that cannot be bypassed. Let’s take a look at how the Go language completes compilation.

Preliminary knowledge

If you want to have an in-depth understanding of the compilation process of the Go language, you need to understand some terminology and professional knowledge involved in the compilation process in advance. This knowledge is actually difficult to use in our daily work and study, but it is still very important for understanding the process and principles of compilation.

1) Abstract Syntax Tree

In computer science, Abstract Syntax Tree (AST), or simply Syntax tree (Syntax tree) , is an abstract representation of the grammatical structure of the source code. It represents the grammatical structure of a programming language in the form of a tree, and each node on the tree represents a structure in the source code.

The reason why grammar is "abstract" is because the grammar here does not represent every detail that appears in real grammar. For example, nested parentheses are implicit in the tree structure and are not represented as nodes. Conditional statements like if else can be represented by nodes with two branches.

Taking the arithmetic expression 1 3*(4-1) 2 as an example, the abstract syntax tree that can be parsed is as shown below:

Does go language need to be compiled?

Figure: Abstract syntax tree

Abstract syntax trees can be used in many fields, such as browsers, smart editors, and compilers.

2) Static single assignment

In compiler design, static single assignment form (static single assignment form, usually abbreviated as SSA form or SSA ) is an attribute of intermediate representation (IR), which requires that each variable is allocated only once, and the variables need to be defined before use. In practice, we usually use the method of adding subscripts to achieve the feature that each variable can only be assigned once. Here is a simple example with the following code:

x := 1
x := 2
y := x
Copy after login

As we know from the above description, first The row assignment behavior is not needed because x is assigned twice in the second line and used in the third line. Under SSA, it will become the following form:

x1 := 1
x2 := 2
y1 := x2
Copy after login

From the middle of using SSA From the code, we can clearly see that the value of variable y1 has absolutely no relationship with x1, so the first step can be omitted when generating machine code, which can reduce the number of instructions that need to be executed to optimize this piece of code.

According to Wikipedia's introduction to SSA, using the features of SSA in intermediate code can achieve the following optimizations for the entire program:

  • Constant propagation (constant propagation)
  • Value range propagation
  • Sparse conditional constant propagation
  • Eliminate dead code elimination)
  • Global value numbering
  • Partial redundancy elimination
  • Strength reduction
  • Register allocation

Because the main function of SSA is code optimization, it is the compiler backend (mainly responsible for the optimization and generation of target code) a part of. Of course, in addition to SSA, there are many intermediate code optimization methods in the field of code compilation. Optimizing the code generated by the compiler is a very old and complex field, which will not be introduced here.

3) Instruction Set Architecture

The last prerequisite knowledge to be introduced is the instruction set architecture, Instruction Set Architecture (ISA) , also known as instruction set or instruction set system, is the part of computer architecture related to programming, including basic data types, instruction sets, registers, addressing modes, storage systems, interrupts, exception handling, and external I/O. The instruction set architecture consists of a series of opcodes (machine language), basic commands that are executed by a specific processor.

Common types of instruction set architectures are as follows:

  • Complex Instruction Set Computing (CISC);
  • Reduced Instruction Set Computing (RISC for short);
  • Explicitly Parallel Instruction Computing (EPIC for short);
  • Very Long Instruction Word Instruction Set Operation (VLIW).


Different processors (CPUs) use very different machine languages, so if our program wants to run on different machines, we need to compile the source code into different machine languages.

Compilation principle

The source code of the Go language compiler is in the cmd/compile directory. The files in the directory together constitute the Go language compiler. People who have studied the principles of compilation may have heard of the front-end and back-end of a compiler. The front-end of the compiler is generally responsible for lexical analysis, syntax analysis, type checking and intermediate code generation, while the back-end of the compiler is mainly responsible for the target code. Generation and optimization, that is, translating the intermediate code into machine code that can be run by the target machine.

Does go language need to be compiled?

Go's compiler can be logically divided into four stages: lexical and syntactic analysis, type checking and AST conversion, general SSA generation and final machine code generation. Below we will introduce the work done in these four stages respectively.

1) Lexical and syntactic analysis

All compilation processes actually start from parsing the source files of the code. The role of lexical analysis is to parse the source Code file, it converts the string sequence in the file into a Token sequence to facilitate subsequent processing and parsing. We generally call the program that performs lexical analysis a lexer (lexer).

The input of grammatical analysis is the Token sequence output by the lexical analyzer. These sequences will be parsed by the grammatical analyzer in order. The parsing process of the grammar is to convert the Token generated by lexical analysis into the grammar defined by the language ( Grammar) bottom-up or top-down specification, each Go source code file will eventually be summarized into a SourceFile structure:

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" }
Copy after login

标准的 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())
Copy after login

接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、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)
    }
}
Copy after login

类型检查会对传入节点的子节点进行遍历,这个过程会对 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()
}
Copy after login

在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。

总结

Go语言的编译过程其实是非常有趣并且值得学习的,通过对Go语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go语言对于我们来讲也不再那么神秘,所以学习其编译原理的过程还是非常有必要的。

更多编程相关知识,请访问:编程视频!!

The above is the detailed content of Does go language need to be compiled?. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template