在实际的工作中,我们很多时候开发环境跟应用程序最终运行的环境是不同的操作系统,比如在 Windows 上进行开发,但是应用程序最终是要在 Linux 上运行的, 又或者是在 mac 下开发,在 Linux 下运行。这个时候我们就需要进行交叉编译,即在一个操作系统上编译出另一个操作系统的可执行文件。
使用 Go 的时候,我们可以很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可,本文会通过一个简单的例子来演示如何进行交叉编译。
GOOS 和 GOARCH 环境变量所有可能的值
在 Go 语言中,我们可以通过设置环境变量 GOOS
和
GOARCH
来指定目标操作系统和目标架构。
比如在我的系统上,查看 GOOS
和 GOARCH
的值:
1 | ➜ go env GOOS GOARCH |
在 Go 编译的时候,默认的 GOOS
和 GOARCH
的值是当前系统的操作系统和架构,比如在我的系统上,GOOS
的值是 darwin
,GOARCH
的值是
amd64
。
所以编译出来的就是当前系统可以执行的二进制文件,如果我们想要编译出其他系统的二进制文件,就需要设置
GOOS
和 GOARCH
的值。
首先,我们需要了解这两个环境变量支持哪些值。下面是所有可能的值,我们可以通过
go tool dist list
列出来:
1 | aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386 |
在上面的输出中,/
前面操作系统,/
后面是架构。以 linux/386
为例,键值对以 GOOS
开始,在本例中将是 linux
,指的是 Linux 操作系统。这里的
GOARCH
将是 386
,代表
Intel 80386
微处理器。
我们发现其实 Go
支持很多操作系统和架构,但是大多数情况下,你最终会使用
linux
、windows
或 darwin
中的一个作为 GOOS
的值,这涵盖了三大操作系统平台:Linux、Windows 和 macOS。
使用文件名后缀实现交叉编译
使用场景:不同操作系统需要通过不同代码来实现。
Go 标准库中 path/filepath
包中的 Join
函数,在不同平台下会有不同的效果。该函数接受一些字符串,并返回一个使用正确文件路径分隔符连接在一起的字符串。
这是一个很好的示例程序,因为程序的操作取决于它运行的操作系统。在
Windows 上,路径分隔符是反斜杠 \
,而 Unix 系统使用正斜杠
/
。
1 | package main |
这个程序在 Windows 上运行时,将输出 a\b\c
,而在 Unix
系统上运行时,将输出 a/b/c
。
这是如何实现的呢?这就涉及到了 Go
中实现交叉编译的其中一种方式,就是指定文件名后缀, 我们看 Go
的源码或者一些开源项目的源码,就会发现有些文件的文件名带了操作系统的后缀,比如
file_windows.go
、file_linux.go
、file_darwin.go
等等。
同样的,path/filepath
包中的 Join
函数也是这样实现的,我们可以看到 path/filepath
包中有很多文件,比如
path_windows.go
、path_unix.go
等等,其中:
path_windows.go
中实现了Join
函数在 Windows 上的实现path_unix.go
中实现了Join
函数在 Unix 系统上的实现
我们点开 path_unix.go
文件,可以看到如下的代码:
1 | // ... |
也就是说,Join
函数的路径分隔符是在这里通过
PathSeparator
定义成 /
的,而在
path_windows.go
文件中,PathSeparator
是定义成
\
的。
1 | // path_windows.go |
有两个
\
是因为需要转义。
同时在文件名中加上 GOARCH 后缀
在命名文件时,您可以按照顺序将 GOOS
和
GOARCH
添加为文件名的后缀,用下划线(_
)分隔这些值。如果您有一个名为
filename.go
的 Go 文件,您可以通过将文件名更改为
filename_GOOS_GOARCH.go
来指定操作系统和架构。例如,如果您希望将其编译为具有 64 位 ARM 架构的
Windows 文件,您将文件名更改为 filename_windows_arm64.go
。这种命名约定有助于保持代码整洁有序。
在我们编译的时候,如果我们当前的 GOOS
和
GOARCH
跟文件名不匹配,则 Go 会忽略这个文件。
使用构建标签实现交叉编译
使用场景:不同操作系统需要使用不同代码。(跟上一个类似)
除了指定文件名后缀以外,我们还可以使用构建标签来实现交叉编译。具体来说,就是在文件的第一行添加
// +build
标签,比如:
1 | // +build windows |
这样的话,这个文件就只会在 Windows 上编译,而在其他系统上不会编译。
使用你本地 GOOS 和 GOARCH 的值进行交叉编译
使用场景:在本地开发环境编译出其他系统的可执行文件。
之前,您运行了 go env GOOS GOARCH
命令来查看您正在使用的操作系统和架构。当您运行 go env
命令时,它会查找两个环境变量 GOOS
和
GOARCH
;如果找到,它们的值将被使用,但如果未找到,则 Go
将使用当前平台的信息来设置它们。这意味着您可以更改 GOOS
或
GOARCH
,以便它们不会默认为您的本地操作系统和架构。这样就可以编译出其他平台的可执行文件。
go build
命令的行为方式类似于 go env
命令。您可以使用 go build
设置 GOOS
或
GOARCH
环境变量以构建不同平台的应用程序。
如果您没有使用 Windows 系统,请在运行 go build
命令时将
GOOS
环境变量设置为 windows
:
1 | GOOS=windows go build |
你也可以同时设置 GOARCH
环境变量:
1 | GOOS=linux GOARCH=amd64 go build |
这将编译出一个 Linux 平台上的 64 位可执行文件,我们如果使用的是
macOS,我们可以通过 file
命令查看编译出来的文件的信息:
1 | file main |
我们会看到这是一个 64 位的 ELF 可执行文件,而 ELF 是 Linux 下的可执行文件格式。
更加现代化的交叉编译方式
我们前面讲了很多如何进行交叉编译,但是如果我们每次都需要针对不同平台来手动编译,未免过于麻烦,当然我们可以写一个脚本来自动化这个过程。
这一小节,我将介绍一个比较好用的交叉编译工具
goreleaser
,我们只需要简单的配置一下,它就可以帮我们自动化交叉编译的过程。
比如 frp
这个开源项目就是使用 goreleaser
来进行发布新版本的。
下面是一个示例配置文件:
1 | # .goreleaser.yml,放在项目根目录下 |
说明:
project_name
是项目的名称before
是在执行前需要执行的命令builds
是编译配置,env
是环境变量,goos
是需要编译的操作系统archives
是归档配置,replacements
是将GOARCH
替换。checksum
是生成 checksum 的配置
接着我们只需要执行 goreleaser build
命令即可进行编译:
goreleaser
的安装方式可参考它的官网。
1 | goreleaser build |
输出:
1 | • starting build... |
编译完成后,我们会在 dist
目录下看到编译好的文件:
1 | ls -l dist/ |
输出:
1 | total 32 |
接着,我们就可以来发布这些二进制文件了。如果我们有其他个性化的需求,我们可以通过修改
.goreleaser.yml
文件来满足我们的需求。它还有很多配置可以自定义。
如果后续我们需要调整,只需要修改一下配置文件就行了,比如我们需要支持一个新的操作系统,只需要在
goos
下面增加一个新的操作系统即可。
使用 goreleaser 进行交叉编译的好处是,它会自动帮我们打包、生成 checksum、生成 changelog 等等,省去了很多手动操作。
另外,它还支持直接发布到 Github,使用 Github Actions 来自动化这个过程,这样我们只需要 push 代码,就可以自动进行编译、打包、发布。
下面是一个 github workflow 的示例配置文件:
1 | name: goreleaser |
这个配置文件的意思是,当我们 push tag 的时候,就会触发这个 workflow,它会自动运行 goreleaser,然后进行编译、打包、发布。
总结
Go 支持我们很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可:
- 环境变量:
GOOS
和GOARCH
- 文件名后缀:
filename_GOOS_GOARCH.go
- 构建标签:
// +build
标签
另外,我们还可以使用 goreleaser
这个工具来自动化交叉编译的过程,它还支持直接发布到 Github,使用 Github
Actions 来自动化这个过程,这样我们只需要 push
tag,就可以自动进行编译、打包、发布。