Heim >Backend-Entwicklung >Golang >Muss die Go-Sprache kompiliert werden?
Go-Sprache muss kompiliert werden. Die Go-Sprache ist eine kompilierte statische Sprache, die zur Ausführung kompiliert werden muss. Mit anderen Worten: Bevor ein Go-Sprachprogramm ausgeführt werden kann, muss ein Compiler einen binären Maschinencode (ausführbare Binärdatei) generieren Die Binärdatei kann auf dem Zielcomputer ausgeführt werden.
Die Betriebsumgebung dieses Tutorials: Windows 7-System, GO Version 1.18, Dell G3-Computer.
Go-Sprache ist eine Programmiersprache, die zum Ausführen eine Kompilierung erfordert. Dies bedeutet, dass der Code vor der Ausführung von einem Compiler generiert werden muss, um binären Maschinencode zu generieren, und dann kann die Binärdatei auf dem Zielcomputer ausgeführt werden.
Einfach ausgedrückt ist die Go-Sprache eine kompilierte statische Sprache (wie die C-Sprache). Bevor ein Go-Sprachprogramm ausgeführt wird, muss es daher in eine binäre ausführbare Datei kompiliert werden.
Wenn wir die Implementierungsprinzipien der Go-Sprache verstehen wollen, ist das Verständnis ihres Kompilierungsprozesses etwas, das nicht umgangen werden kann. Werfen wir einen Blick darauf, wie die Go-Sprache die Kompilierung abschließt.
Wenn Sie den Kompilierungsprozess der Go-Sprache gründlich verstehen möchten, müssen Sie im Voraus einige Terminologie- und Fachkenntnisse verstehen, die mit dem Kompilierungsprozess verbunden sind. Dieses Wissen ist in unserer täglichen Arbeit und unserem Studium tatsächlich schwer anzuwenden, aber es ist dennoch sehr wichtig für das Verständnis des Prozesses und der Prinzipien der Kompilierung.
In der Informatik ist Abstract Syntax Tree (AST), oder kurz Syntaxbaum, eine abstrakte Darstellung der grammatikalischen Struktur von Quellcode. Es stellt die grammatikalische Struktur einer Programmiersprache in Form eines Baums dar, und jeder Knoten im Baum repräsentiert eine Struktur im Quellcode.
Der Grund, warum Grammatik „abstrakt“ ist, liegt darin, dass die Grammatik hier nicht jedes Detail darstellt, das in der echten Grammatik vorkommt. Verschachtelte Klammern sind beispielsweise in der Baumstruktur implizit und werden nicht als Knoten dargestellt. Bedingte Anweisungen wie if else können durch Knoten mit zwei Zweigen dargestellt werden.
Nehmen Sie den arithmetischen Ausdruck 1+3*(4-1)+2 als Beispiel. Der abstrakte Syntaxbaum, der analysiert werden kann, ist in der folgenden Abbildung dargestellt:
Abbildung: Abstrakter Syntaxbaum
Abstrakte Syntaxbäume können in vielen Bereichen verwendet werden, z. B. in Browsern, intelligenten Editoren und Compilern.
Im Compiler-Design ist die statische Einzelzuweisungsform (statische Einzelzuweisungsform, normalerweise als SSA-Form oder SSA abgekürzt) ein Attribut der Zwischendarstellung (IR, Zwischendarstellung). müssen nur einmal zugewiesen werden, und Variablen müssen vor der Verwendung definiert werden. In der Praxis verwenden wir normalerweise das Hinzufügen von Indizes, um die Funktion zu erreichen, dass jede Variable nur einmal zugewiesen werden kann. Hier ist ein einfaches Beispiel mit dem folgenden Code:
x := 1 x := 2 y := x
Wie wir aus der obigen Beschreibung wissen, ist die erste Zeile des Zuweisungsverhaltens It ist nicht notwendig, da x in der zweiten Zeile zweimal zugewiesen und in der dritten Zeile verwendet wird. Unter SSA hat es die folgende Form:
x1 := 1 x2 := 2 y1 := x2
Wir können es sehr deutlich aus dem Zwischencode erkennen, der SSA verwendet. Der Wert der Variablen y1 hat absolut nichts mit x1 zu tun, daher kann der erste Schritt beim Generieren von Maschinencode weggelassen werden, wodurch die Anzahl der Anweisungen reduziert wird, die zur Optimierung dieses Codeteils ausgeführt werden müssen.
Laut Wikipedias Einführung in SSA können durch die Verwendung der Funktionen von SSA im Zwischencode die folgenden Optimierungen für das gesamte Programm erreicht werden:
Registerzuweisung
Da die Hauptfunktion von SSA die Codeoptimierung ist, ist es das Compiler-Backend (hauptsächlich verantwortlich für die Optimierung und Generierung von Zielcode) Teil von. Zusätzlich zu SSA gibt es im Bereich der Codekompilierung natürlich viele Zwischencodeoptimierungsmethoden. Die Optimierung des vom Compiler generierten Codes ist ein sehr altes und komplexes Gebiet, das hier nicht vorgestellt wird.
Das letzte erforderliche Wissen ist die Befehlssatzarchitektur (ISA), die auch als Befehlssatz- oder Befehlssatzarchitektur bezeichnet wird Die Programmierung umfasst grundlegende Datentypen, Befehlssätze, Register, Adressierungsmodi, Speichersysteme, Interrupts, Ausnahmebehandlung und externe E/A. Die Befehlssatzarchitektur besteht aus einer Reihe von Opcodes (Maschinensprache), grundlegenden Befehlen, die von einem bestimmten Prozessor ausgeführt werden.
Übliche Arten von Befehlssatzarchitekturen sind wie folgt:
Unterschiedliche Prozessoren (CPUs) verwenden sehr unterschiedliche Maschinensprachen. Wenn unser Programm also auf verschiedenen Maschinen ausgeführt werden soll, müssen wir den Quellcode entsprechend der Architektur in verschiedene Maschinensprachen kompilieren.
Der Quellcode des Go-Sprachcompilers befindet sich im Verzeichnis cmd/compile. Die Dateien im Verzeichnis bilden zusammen den Go-Sprachcompiler Das Front-End des Compilers ist im Allgemeinen für die lexikalische Analyse, die Syntaxanalyse, die Typprüfung und die Zwischencode-Generierung verantwortlich des Zielcodes, d. h. die Übersetzung des Zwischencodes in den ausführbaren Zielmaschinencode.
Der Compiler von Go kann logisch in vier Phasen unterteilt werden: lexikalische und syntaktische Analyse, Typprüfung und AST-Konvertierung, allgemeine SSA-Generierung und endgültige Maschinencode-Generierung.
Alle Kompilierungsprozesse beginnen tatsächlich mit dem Parsen der Quellcodedateien. Die Funktion der lexikalischen Analyse besteht darin, die Quellcodedateien zu analysieren, wodurch die Zeichenfolgensequenzen in den Dateien in Token umgewandelt werden Sequenz, um die anschließende Verarbeitung und Analyse zu erleichtern. Wir nennen das Programm, das die lexikalische Analyse durchführt, im Allgemeinen einen Lexer.
Die Eingabe der grammatikalischen Analyse ist die vom lexikalischen Analysator ausgegebene Token-Sequenz. Diese Sequenzen werden vom grammatikalischen Analysator analysiert, um den durch die lexikalische Analyse generierten Token gemäß der sprachdefinierten Grammatik zu konvertieren. Grammatik). Durch Top-Down- oder Top-Down-Spezifikation wird jede Go-Quellcodedatei schließlich in einer SourceFile-Struktur zusammengefasst:
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语言对于我们来讲也不再那么神秘,所以学习其编译原理的过程还是非常有必要的。
更多编程相关知识,请访问:编程视频!!
Das obige ist der detaillierte Inhalt vonMuss die Go-Sprache kompiliert werden?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!