学习一门新的编程语言最令人欣慰的部分之一是最终运行可执行文件并获得期望的输出。当我发现 Go 编程语言时,我首先阅读了一些示例程序来熟悉语法,然后编写了小型测试程序。随着时间的推移,这种方法帮助我熟悉了程序的编译和构建。
Go 可用的构建选项提供了更多控制构建过程的方法。它们还可以提供额外的信息,以帮助将过程分解为更小的部分。在本文中,我将演示我使用过的一些选项。注意:我使用术语构建和编译来表示相同的含义。
Go 入门
我正在使用 Go 版本 1.16.7;但是,这里给出的命令也应该适用于大多数最新版本。如果您没有安装 Go,您可以从 Go 网站下载并按照安装说明进行操作。通过打开命令提示符并键入以下内容来验证您安装的版本
$ go version
响应应如下所示,具体取决于您的版本。
go version go1.16.7 linux/amd64
$
Go 程序的基本编译和执行
我将从一个简单的 Go 程序开始,该程序仅在屏幕上打印“Hello World”。
$ cat hello.go
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
$
在讨论更高级的选项之前,我将解释如何编译一个简单的 Go 程序。我使用 build
选项,后跟 Go 程序源文件名,在本例中为 hello.go
。
$ go build hello.go
如果一切正常,您应该在当前目录中看到一个名为 hello
的可执行文件。您可以使用 file 命令验证它是否为 ELF 二进制可执行格式(在 Linux 平台上)。您也可以执行它并查看它是否输出“Hello World”。
$ ls
hello hello.go
$
$ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$
$ ./hello
Hello World
$
Go 提供了一个方便的 run
选项,以防您不想拥有生成二进制文件,而是想查看程序是否正常工作并打印所需的输出。请记住,即使您在当前目录中看不到可执行文件,Go 仍然会编译并在某处生成可执行文件、运行它,然后将其从系统中删除。我将在本文的后面部分解释。
$ go run hello.go
Hello World
$
$ ls
hello.go
$
幕后原理
上述命令可以像微风一样轻松运行我的程序,并且只需最少的努力。但是,如果您想了解 Go 在幕后做了什么来编译这些程序,Go 提供了 -x
选项,该选项会打印 Go 为生成可执行文件所做的所有操作。
快速浏览一下,您就会发现 Go 在 /tmp
中创建了一个临时工作目录,生成了可执行文件,然后将其移动到源 Go 程序所在的当前目录。
$ go build -x hello.go
WORK=/tmp/go-build1944767317
mkdir -p $WORK/b001/
<< snip >>
mkdir -p $WORK/b001/exe/
cd .
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK \
/b001/exe/a.out -importcfg $WORK/b001 \
/importcfg.link -buildmode=exe -buildid=K26hEYzgDkqJjx2Hf-wz/\
nDueg0kBjIygx25rYwbK/W-eJaGIOdPEWgwC6o546 \
/K26hEYzgDkqJjx2Hf-wz -extld=gcc /root/.cache/go-build /cc \
/cc72cb2f4fbb61229885fc434995964a7a4d6e10692a23cc0ada6707c5d3435b-d
/usr/lib/golang/pkg/tool/linux_amd64/buildid -w $WORK \
/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out hello
rm -r $WORK/b001/
当程序运行但当前目录中未创建任何结果可执行文件时,这有助于解决困惑。使用 -x
表明可执行文件确实在 /tmp
工作目录中创建并已执行。但是,与 build
选项不同,可执行文件没有移动到当前目录,这使得它看起来好像没有创建任何可执行文件。
$ go run -x hello.go
mkdir -p $WORK/b001/exe/
cd .
/usr/lib/golang/pkg/tool/linux_amd64/link -o $WORK/b001 \
/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=hK3wnAP20DapUDeuvAAS/E_TzkbzwXz6tM5dEC8Mx \
/7HYBzuaDGVdaZwSMEWAa/hK3wnAP20DapUDeuvAAS -extld=gcc \
/root/.cache/go-build/75/ \
7531fcf5e48444eed677bfc5cda1276a52b73c62ebac3aa99da3c4094fa57dc3-d
$WORK/b001/exe/hello
Hello World
模拟编译但不生成可执行文件
假设您不想编译程序并生成实际的二进制文件,但您确实想查看过程中的所有步骤。您可以使用 -n
构建选项来执行此操作,该选项会打印它通常会运行的步骤,而无需实际创建二进制文件。
$ go build -n hello.go
保存临时目录
大量工作发生在 /tmp
工作目录中,一旦创建并运行可执行文件,该目录就会被删除。但是,如果您想查看在编译过程中创建了哪些文件怎么办?Go 提供了一个 -work
选项,可以在编译程序时使用。-work
选项除了运行程序外,还会打印工作目录路径,但之后不会删除工作目录,因此您可以移动到该目录并检查编译过程中创建的所有文件。
$ go run -work hello.go
WORK=/tmp/go-build3209320645
Hello World
$
$ find /tmp/go-build3209320645
/tmp/go-build3209320645
/tmp/go-build3209320645/b001
/tmp/go-build3209320645/b001/importcfg.link
/tmp/go-build3209320645/b001/exe
/tmp/go-build3209320645/b001/exe/hello
$
$ /tmp/go-build3209320645/b001/exe/hello
Hello World
$
其他编译选项
如果,您想手动编译程序并最终得到一个可以直接由您的操作系统(在本例中为 Linux)运行的可执行文件,而不是使用 Go 的构建/运行魔法,该怎么办?此过程可以分为两个部分:编译和链接。使用 tool
选项查看其工作原理。
首先,使用 tool compile
选项生成结果 ar
存档文件,其中包含 .o
中间文件。接下来,对此 hello.o
文件使用 tool link
选项以生成最终可执行文件,然后可以运行该文件。
$ go tool compile hello.go
$
$ file hello.o
hello.o: current ar archive
$
$ ar t hello.o
__.PKGDEF
_go_.o
$
$ go tool link -o hello hello.o
$
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$
$ ./hello
Hello World
$
要进一步了解从 hello.o
文件生成可执行文件的链接过程,您可以使用 -v
选项,该选项会搜索每个 Go 可执行文件中包含的 runtime.a
文件。
$ go tool link -v -o hello hello.o
HEADER = -H5 -T0x401000 -R0x1000
searching for runtime.a in /usr/lib/golang/pkg/linux_amd64/runtime.a
82052 symbols, 18774 reachable
1 package symbols, 1106 hashed symbols, 77185 non-package symbols, 3760 external symbols
81968 liveness data
$
交叉编译选项
现在我已经解释了 Go 程序的编译,我将演示 Go 如何允许您通过在实际 build
命令之前提供两个环境变量——GOOS 和 GOARCH——来构建针对不同硬件架构和操作系统的可执行文件。
为什么这很重要?您可以看到一个示例,即为 ARM (aarch64) 架构生成的可执行文件无法在 Intel (x86_64) 架构上运行,并产生 Exec 格式错误。
这些选项使得生成跨平台二进制文件变得非常简单。
$ GOOS=linux GOARCH=arm64 go build hello.go
$
$ file ./hello
./hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
$
$ ./hello
bash: ./hello: cannot execute binary file: Exec format error
$
$ uname -m
x86_64
$
您可以阅读我之前关于使用 Go 进行交叉编译 的经验的博客文章,以了解更多信息。
查看底层汇编指令
源代码不会直接转换为可执行文件,尽管它会生成一个中间汇编格式,然后将其汇编为可执行文件。在 Go 中,这映射到中间汇编格式,而不是底层硬件汇编指令。
要查看此中间汇编格式,请使用 -gcflags
,后跟 -S
给构建命令。此命令显示汇编指令。
$ go build -gcflags="-S" hello.go
# command-line-arguments
"".main STEXT size=138 args=0x0 locals=0x58 funcid=0x0
0x0000 00000 (/test/hello.go:5) TEXT "".main(SB), ABIInternal, $88-0
0x0000 00000 (/test/hello.go:5) MOVQ (TLS), CX
0x0009 00009 (/test/hello.go:5) CMPQ SP, 16(CX)
0x000d 00013 (/test/hello.go:5) PCDATA $0, $-2
0x000d 00013 (/test/hello.go:5) JLS 128
<< snip >>
$
您也可以使用 objdump -s
选项,如下所示,查看已编译的可执行程序的汇编指令。
$ ls
hello hello.go
$
$
$ go tool objdump -s main.main hello
TEXT main.main(SB) /test/hello.go
hello.go:5 0x4975a0 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
hello.go:5 0x4975a9 483b6110 CMPQ 0x10(CX), SP
hello.go:5 0x4975ad 7671 JBE 0x497620
hello.go:5 0x4975af 4883ec58 SUBQ $0x58, SP
hello.go:6 0x4975d8 4889442448 MOVQ AX, 0x48(SP)
<< snip >>
$
剥离二进制文件以减小其大小
Go 二进制文件通常很大。例如,一个简单的 Hello World 程序会生成一个 1.9M 大小的二进制文件。
$ go build hello.go
$
$ du -sh hello
1.9M hello
$
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$
要减小结果二进制文件的大小,您可以剥离执行期间不需要的信息。使用 -ldflags
,后跟 -s -w
标志,使结果二进制文件稍微轻巧一些,为 1.3M。
$ go build -ldflags="-s -w" hello.go
$
$ du -sh hello
1.3M hello
$
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$
结论
我希望本文向您介绍了一些方便的 Go 构建选项,这些选项可以帮助您更好地理解 Go 编译过程。有关构建过程和可用的其他有趣选项的更多信息,请参阅帮助部分
$ go help build
评论已关闭。