原来go build命令有这么多学问

2024-09-09 23:16:00 浏览数 (2)

最近在写一个小项目时发现一个问题,首先模拟一下我这个Demo的场景:

简单来说有两个go文件组成的Demo,分别是hello.gomain.go,其中main.go中的main函数需要引用hello.go中的printHello()函数,但是在编译项目的时候突然报错了,代码和文件关系如下:

文件关系:

代码语言:shell复制
├── go.mod
├── hello.go
└── main.go

hello.go

代码语言:go复制
package main

import "fmt"

func printHello() {
    fmt.Println("Hello ...")
}

main.go

代码语言:go复制
package main

func main() {
    printHello()
}

下面我想把这个Demo编译成可执行文件,当我使用go build main.go时就报错了,如下:

代码语言:go复制
# command-line-arguments
./main.go:4:5: undefined: printHello

这个报错说明main.go中找不到printHello这个函数的定义,但是我们主观上一定会觉得它是能找到这个函数的,毕竟都在同一级目录仅是不同文件而已,但就是编译不过,于是我使用go build命令,就直接成功了,后来我查了一下go build命令,原来它的学问还不少!

首先,go build命令是干什么的?

go build 命令是 Go 语言提供的用于编译 Go 程序的工具。当你运行 go build 时,它会查找当前目录或指定目录下的 .go 源代码文件,编译它们,并生成可执行文件(在 Windows 上是 .exe 文件,在 Unix-like 系统上是没有扩展名的文件)。默认情况下,生成的可执行文件的名字与包含 main 函数的包名相同(但通常是你目录下的第一个 .go 文件所在的包名,除非你在命令行中明确指定了输出文件名)。

go build 命令的基本用法如下:

代码语言:bash复制
go build [options] [packages]
  • options:可选的编译选项,用于控制编译过程。例如,-o 选项允许你指定输出文件的名称。
  • packages:要编译的包名列表。如果省略,go build 会编译当前目录及其子目录下的所有 .go 文件。

1)常用选项

  • -o file:指定输出文件的名称。如果未指定,则输出文件名默认为包名(第一个非 _ 开头的 .go 文件所在的包名)。
  • -i:安装依赖包但不编译目标代码。这个选项在 Go 1.11 及以后的版本中已被弃用,因为现在的 go build 会根据需要自动安装依赖。
  • -v:打印出编译过程中被编译的包名。
  • -work:打印出用于编译的临时工作目录的路径(但请注意,.o 文件仍然可能会被清理)。
  • -x:打印出执行的具体命令,包括编译和链接过程中使用的命令。

2)示例

编译当前目录的 Go 程序:

代码语言:bash复制
go build

这会在当前目录下生成一个可执行文件(文件名根据包名而定)。

指定输出文件名:

代码语言:bash复制
go build -o myapp

这会生成一个名为 myapp 的可执行文件(在 Unix-like 系统上)或 myapp.exe(在 Windows 上)。

编译特定包:

代码语言:bash复制
go build github.com/user/repo

这会编译指定仓库(GitHub 上的 user/repo)中的 Go 程序,并生成可执行文件(如果包中包含 main 函数的话)。但是,请注意,如果你没有在该仓库的根目录下运行此命令,那么生成的可执行文件通常会被放置在你的 GOPATH/bin 目录下(如果你设置了 GOPATH 的话),或者在当前目录下(如果你使用的是 Go Modules,并且没有设置 GOPATH)。

3)注意

  • go build 命令不会输出编译过程中的错误信息到标准输出(stdout),而是将它们输出到标准错误(stderr)。这意味着你可以通过重定向标准输出来捕获编译结果,而不会与错误信息混淆。
  • 在使用 Go Modules 时,go build 会根据 go.mod 文件中的依赖关系来编译你的程序,并自动下载任何缺失的依赖包。
  • 默认情况下,go build 不会输出任何编译过程中的详细信息,除非你使用了 -v-x 选项。
go build和go build main.go有什么不一样?

go build 命令和 go build main.go 命令在 Go 语言中用于编译 Go 程序,但它们之间存在一些关键的不同点,主要涉及到 Go 的包(package)结构和编译范围。

1)go build

当仅仅在包含 main 包的目录(或其父目录)中运行 go build 命令时,Go 工具链会查找当前目录(及其子目录)下的所有 .go 文件,构建一个包含所有相关依赖的二进制可执行文件。这个二进制文件的名字默认为当前目录的名称(但去掉任何可能的路径分隔符),并且不包含 .go 后缀。例如,如果你的目录名为 myapp,并且该目录或其子目录中包含 main 包,那么 go build 将生成一个名为 myapp(或 myapp.exe 在 Windows 上)的可执行文件。

2)go build main.go

当你使用 go build main.go 命令时,Go 工具链的行为会有所不同。它会将 main.go 文件视为一个独立的编译单元,并尝试仅根据 main.go 文件中直接导入的包来构建程序。 这意味着,如果 main.go 依赖于当前目录或其他目录中的其他 .go 文件,但这些依赖没有通过 import 语句在 main.go 中显式引入,那么这些依赖可能不会被包含在最终的二进制文件中,这可能导致编译错误或运行时错误。

此外,使用 go build main.go 时,生成的二进制文件名可能会基于 main.go 所在的目录名,而不是 main.go 文件的内容或其中的包名。然而,具体的行为可能因 Go 的版本和具体环境而异。

总而言之,推荐使用 go build,因为它遵循 Go 的包管理原则,能够自动处理当前目录及其子目录下的所有相关 .go 文件,确保所有依赖都被正确包含。避免使用 go build main.go,除非确实需要仅编译 main.go 文件,并且确信它包含了所有必要的依赖。通常,这种用法不推荐,因为它可能会忽略重要的代码和依赖,导致不稳定的程序或意外的编译错误。

到这里我们大概明白了,因为在使用go build main.go时只使用了main.go文件,那么我们怎样改进呢?答案是将关联的文件一并编译

代码语言:shell复制
go build -o demo main.go hello.go

这样的话就能顺利编译通过啦~

回顾Go编译过程

其实从宏观上来看,几乎所有高级编程语言(Java、C、Go、C 等)的编译过程都如出一辙,基本上都包括词法分析、语法分析、语义分析、中间代码生成、目标代码优化和链接这几个过程,只是编译器是使用有所不同。

1)词法分析(Lexical Analysis) :将源代码分解为标记(tokens)。标记是源代码中的基本单位,如标识符、关键字、运算符、界定符等。词法分析器(Lexer)使用正则表达式来识别源代码中的字符序列,并将其转换为标记。这些标记随后被传递给语法分析器。

2)语法分析(Syntax Analysis) :将词法分析阶段生成的标记组合成语法结构(如表达式、语句、函数等)。语法分析器(Parser)使用上下文无关文法(CFG)来解析这些标记,并构建出一个抽象语法树(AST)。AST是源代码的树状表示,反映了程序的语法结构。

3)语义分析(Semantic Analysis) :检查语法结构的语义正确性,包括类型检查、类型推断、函数内联等优化工作。在这一阶段,编译器会验证代码的类型安全性,确保所有的变量和表达式都有正确的类型。此外,Go编译器还会进行类型推断,对于使用字面量初始化的变量,编译器会自动推断其类型。同时,编译器还会对代码进行优化,如函数内联,以减少函数调用的开销。

4)中间代码生成(Code Generation): 将语义分析后的语法结构转换为机器代码或中间代码(IR)。在这一阶段,编译器会将AST转换为一种更低级的表示形式,通常是中间代码(IR)。中间代码是一种与具体平台无关的代码表示,它更容易被转换为不同平台的机器代码。然后,编译器会将中间代码转换为目标平台的机器代码。

5)目标代码优化(Target Code Optimization) :在某些情况下,编译器可能在生成目标代码后进一步进行优化,以提高目标代码的执行效率。

6)链接(Linking) :将多个编译单元(如多个.o文件)和必要的库文件链接成一个可执行文件。链接器(Linker)会处理所有编译生成的机器代码文件,以及程序所需的任何库文件,将它们合并成一个单一的可执行文件。这个可执行文件包含了程序运行所需的所有指令和数据。

其中,词法分析、语法分析、语义分析属于编译前端,剩下的属于编译后端。

编译前端是编译器的第一个阶段,主要负责处理源代码的词法分析和语法分析,以及生成中间表示(如中间代码或抽象语法树)。它是将源代码转化为编译器内部可以处理的中间形式的关键步骤。

编译后端是编译器的第二个阶段,主要负责代码优化和目标代码生成。它基于前端生成的中间表示,对代码进行优化,并最终生成目标机器的机器语言代码。

前端负责源代码的词法分析和语法分析,以及生成中间表示;后端则负责代码优化和目标代码生成。两者之间的紧密协作确保了编译过程的顺利进行和最终生成代码的高效执行。

小总结

go build 是 Go 语言中一个非常强大且灵活的命令,它使得从源代码到可执行文件的转换变得简单而直接。通过利用它的各种选项和跨平台编译功能,你可以轻松地编译和分发你的 Go 程序。无论是在开发过程中还是在准备部署时,go build 都是 Go 程序员工具箱中不可或缺的一部分。

与此同时,编译原理确实是一门非常值得深入学习的课程,它不仅在理论层面上构筑了计算机科学的重要基石,更在实践应用中展现了其无可替代的价值。通过学习编译原理,我们能够系统地掌握程序语言的设计原理、编译器的构造技术和优化方法,这对于提升编程能力、理解计算机系统底层运作机制以及进行高效软件开发都具有重要意义。

0 人点赞