Go 言語をコンパイルする必要があります。 Go 言語はコンパイルされた静的言語です。実行するにはコンパイルする必要があるプログラミング言語です。つまり、Go 言語プログラムを実行する前に、コンパイラはバイナリ マシン コード (バイナリ実行可能ファイル) を生成する必要があります。バイナリ ファイルはターゲット マシン上で実行できます。
このチュートリアルの動作環境: Windows 7 システム、GO バージョン 1.18、Dell G3 コンピューター。
Go 言語は、実行するためにコンパイルが必要なプログラミング言語です。つまり、実行前にコンパイラによってコードを生成してバイナリ マシン コードを生成する必要があり、その後、そのバイナリ ファイルをターゲット マシンで実行できます。 。
簡単に言えば、Go 言語はコンパイルされた静的言語 (C 言語と同じ) であるため、Go 言語プログラムを実行する前に、バイナリ実行可能ファイルにコンパイルする必要があります。
Go 言語の実装原理を理解したい場合、そのコンパイル プロセスを理解することは避けては通れません。 Go 言語がどのようにコンパイルを完了するかを見てみましょう。
Go 言語のコンパイル プロセスを深く理解したい場合は、コンパイル プロセスに関連するいくつかの用語と専門知識を事前に理解する必要があります。この知識を日常の仕事や勉強に活用するのは実際には難しいですが、それでも編纂のプロセスや原則を理解するためには非常に重要です。
コンピュータ サイエンスでは、抽象構文ツリー (AST)、または単に構文ツリー (構文ツリー) は抽象的な表現です。ソースコードの文法構造。これはプログラミング言語の文法構造をツリー形式で表し、ツリー上の各ノードはソース コード内の構造を表します。
文法が「抽象的」である理由は、ここでの文法が実際の文法に現れるすべての詳細を表しているわけではないからです。たとえば、ネストされた括弧はツリー構造内で暗黙的に存在し、ノードとして表されません。 if else のような条件文は、2 つの分岐を持つノードで表すことができます。
算術式 1 3*(4-1) 2 を例にとると、解析できる抽象構文ツリーは次のとおりです。
図: 抽象構文ツリー
##抽象構文ツリーは、ブラウザ、スマート エディタ、コンパイラなどの多くの分野で使用できます。
コンパイラー設計では、静的単一代入形式 (静的単一代入形式、通常 SSA 形式または SSA と略されます) は、中間表現 (IR) の属性。各変数を 1 回だけ割り当てる必要があり、変数は使用前に定義する必要があります。実際には、各変数に 1 回だけ代入できるという機能を実現するために、添字を追加する方法がよく使用されます。ここでは、次のコードによる簡単な例を示します:
x := 1 x := 2 y := x
上記の説明からわかるように、まず、 2行目でxが2回代入され、3行目で使用されているため行代入動作は不要 SSAでは以下の形になります:
x1 := 1 x2 := 2 y1 := x2
SSA使用途中から コードより、変数 y1 の値は x1 とまったく関係がないことがはっきりとわかります。そのため、マシンコードを生成するときに最初のステップを省略できます。これにより、このコード部分を最適化するために実行する必要がある命令の数を減らすことができます。
Wikipedia の SSA の紹介によると、中間コードで SSA の機能を使用すると、プログラム全体で次の最適化を実現できます。
レジスタの割り当て
SSA の主な機能はコードの最適化であるため、それはコンパイラのバックエンドです (主に最適化とターゲットコードの生成)の一部です。もちろん、コードのコンパイルの分野には SSA 以外にも中間コードの最適化手法が数多くありますが、コンパイラによって生成されたコードの最適化は非常に古く複雑な分野であるため、ここでは紹介しません。
最後に紹介する前提知識は、命令セット アーキテクチャである命令セット アーキテクチャ (ISA) (命令とも呼ばれます) です。セットまたは命令セット システムとは、基本的なデータ型、命令セット、レジスタ、アドレッシング モード、ストレージ システム、割り込み、例外処理、外部 I/O など、プログラミングに関連するコンピュータ アーキテクチャの一部です。命令セット アーキテクチャは、特定のプロセッサによって実行される基本的なコマンドである一連のオペコード (機械語) で構成されています。
一般的な命令セット アーキテクチャの種類は次のとおりです:
異なるプロセッサ (CPU) は非常に異なる機械語を使用するため、プログラムを異なるマシン上で実行したい場合は、ソース コードを異なる機械語にコンパイルする必要があります。
Go 言語コンパイラのソース コードは cmd/compile ディレクトリにあり、ディレクトリ内のファイルは一緒になって Go 言語コンパイラを構成しますコンパイルの原理を学習したことがある人は、コンパイラのフロントエンドとバックエンドについて聞いたことがあるかもしれません。コンパイラのフロントエンドは通常、字句解析、構文解析、型チェック、中間コード生成を担当します。コンパイラのバックエンドは主にターゲット コードの生成と最適化、つまり中間コードをターゲット マシンで実行できるマシン コードに変換する役割を担います。
Go のコンパイラは論理的に 4 つの段階に分けることができます: 字句解析と構文解析、型チェックと AST 変換、一般的な SSA 生成、最終的なマシンコード生成です。これらの 4 つの段階でそれぞれ作業が行われます。
すべてのコンパイル プロセスは、実際にはコードのソース ファイルを解析することから始まります。ソース コード ファイルを読み込むと、ファイル内の文字列シーケンスがトークン シーケンスに変換され、後続の処理と解析が容易になります。一般に、字句解析を実行するプログラムをレクサー (レクサー) と呼びます。
文法解析の入力は、字句解析によって出力されたトークンのシーケンスです。これらのシーケンスは、文法アナライザによって順番に解析されます。文法の解析プロセスは、字句解析によって生成されたトークンをトークンに変換することです。言語 (文法) のボトムアップ仕様またはトップダウン仕様によって定義された文法に従って、各 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 中国語 Web サイトの他の関連記事を参照してください。