在不同的操作系统和架构编译 Go 应用
在软件开发中,重要的是要考虑你想为之编译二进制的操作系统和底层处理器架构。因为在不同的操作系统/架构平台上运行一个二进制文件通常很慢或不可能,所以通常的做法是为许多不同的平台编译你最终的二进制文件,以最大化你的程序的受众。然而,这通常是很困难的,当你开发软件的平台和你想要部署的平台不是同一个的时候。例如,在过去,在 Windows 上开发一个程序并将其部署到 Linux 或 macOS 机器上,需要为每一个你想要的二进制文件的环境设置构建机器。你还需要保持你的工具同步,此外还有其他考虑因素,这些因素会增加成本,使协作测试和分布式更加困难。
Go 通过在go build
工具中直接建立对多平台的支持,以及 Go 工具链的其他部分解决了这个问题。通过使用环境变量和构建标签,你可以控制你最终的二进制文件是为哪个操作系统和架构构建的,此外还可以把一个工作流程放在一起,在不改变你的代码库的情况下快速切换对平台依赖的代码。
在本教程中,你将把一个将strings连接成文件路径的示例应用程序放在一起,创建并有选择地包括与平台有关的片段,并在你自己的系统上为多个操作系统和系统架构构建二进制文件,向你展示如何使用 Go 编程语言的这一强大能力。
前期准备
为了跟随本文的例子,你将需要:
- 按照如何安装 Go 和设置本地程序环境设置的 Go 的 workspace
GOOS
和GOARCH
可能支持的平台
在展示如何控制构建过程为不同的平台构建二进制文件之前,让我们先了解一下 Go 能够为哪些类型的平台进行构建,以及 Go 如何使用环境变量GOOS
和GOARCH
关联这些平台。
Go 工具有一个命令,可以打印出 Go 可以构建的平台的列表。这个列表会随着每一个新的 Go 版本而改变,所以这里讨论的组合在另一个版本的 Go 中可能不一样。当下写这个教程的时候,Go release 版本是 1.13.
为了找到适用的平台,执行如下命令:
go tool dist list
你将会收到如下相似的输出:
Output
aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386
android/386 freebsd/arm linux/ppc64 openbsd/amd64
android/amd64 illumos/amd64 linux/ppc64le openbsd/arm
android/arm js/wasm linux/s390x openbsd/arm64
android/arm64 linux/386 nacl/386 plan9/386
darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64
darwin/amd64 linux/arm nacl/arm plan9/arm
darwin/arm linux/arm64 netbsd/386 solaris/amd64
darwin/arm64 linux/mips netbsd/amd64 windows/386
dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64
freebsd/386 linux/mips64le netbsd/arm64 windows/arm
输出是一些以/
分割的键值对。键值对的第一个部分,在/
之前的是操作系统。在 Go 里面,这些操作系统会是环境变量GOOS
的值,发音像“goose”,代表Go Operation System。第二部分,在/
之后的,是架构。如前所述,这些都是环境变量GOARCH
可能的值。这个发音”gore-ch”,代表Go Architecture。
让我们以linux/386
为例,对其中的一个组合进行分解,了解它的含义和工作原理。键值对以GOOS
开始,在这个例子中是linux
,指的是Linux 操作系统。这里的GOARCH
应该是386
,它代表英特尔 80386 微处理器。
有许多平台可以使用go build
命令,但大多数情况下,你最终会使用linux
, windows
或darwin
作为 GOOS 的值。这些涵盖了三大操作系统平台: Linux、Windows和macOS,后者是基于Darwin Operating system的,因此被称为darwin
。然而,Go 也可以覆盖不太主流的平台,如nacl
,它代表了谷歌的本地客户端。
当你运行go build
这样的命令时,Go 使用当前平台的GOOS
和GOARCH
来决定如何构建二进制文件。要想知道你的平台是什么组合,你可以使用go env
命令,并将GOOS
和GOARCH
作为参数:
go env GOOS GOARCH
在测试这个例子时,我们在一台AMD64 架构的机器上的 macOS 上运行这个命令,所以我们将收到以下输出:
Output
darwin
amd64
这个命令的输出告诉我们系统的 GOOS 是 darwin,GOARCH 是 amd64。
你现在知道了 Go 中的GOOS
和GOARCH
是什么,以及它们的可能值。接下来,你将编写一个程序,作为如何使用这些环境变量和构建标签为其他平台构建二进制文件的例子。
用filepath.Join
编写一个平台依赖的应用程序
在你开始构建其他平台的二进制前,让我们先构建一个范例程序。出于这个目的,一个好的例子可以用 Go 标准库中的path/filepath包内的Join
函数。这个函数以多个 string 为传参,并返回一个用正确文件路径分隔符拼接的 string。
这是一个很好的范例程序,因为该程序的运行取决于它在哪个操作系统上运行。在 Windows 上,路径分隔符是反斜杠,\
,而基于 Unix 的系统使用正斜杠,/
。
让我们从构建一个使用filepath.Join()
的应用程序开始,稍后,你将编写你自己的Join()
函数的实现,将代码定制为特定平台的二进制文件。
首先,在你的src
目录下创建一个文件夹,用你的应用程序的名字命名:
mkdir app
进入目录:
cd app
接下来,在你选择的文本编辑器中创建一个名为main.go
的新文件。在本教程中,我们将使用 Nano。
nano main.go
文件打开后,添加如下代码:
package main
import (
"fmt"
"path/filepath"
)
func main() {
s := filepath.Join("a", "b", "c")
fmt.Println(s)
}
在这个文件的main()
函数用filepath.Join()
将三个strings用正确的,平台依赖的路径分隔符连接起来。
保存并退出文件,然后运行程序:
go run main.go
当运行这个程序时,你将收到不同的输出,这取决于你所使用的平台。在 Windows 上,你会看到用\
分隔的字符串。
Output
a\b\c
在 MacOS 和 Linux 等 Unix 系统上,你将收到以下内容。
Output
a/b/c
这表明,由于这些操作系统使用的文件系统协议不同,程序将不得不为不同的平台构建不同的代码。但由于它已经根据操作系统使用了不同的文件分隔符,所有我们知道filepath.Join()
已经考虑了平台的差异。这是因为 Go 工具链会自动检测你的机器的GOOS
和GOARCH
,并使用这些信息来使用具有正确构建标签和文件分隔符的代码片段。
让我们思考一下filepath.Join()
函数的分隔符是从哪里来的。运行以下命令来查看 Go 标准库中的相关片段:
less /usr/local/go/src/os/path_unix.go
这将显示path_unix.go
的内容。寻找该文件的如下部分:
. . .
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
package os
const (
PathSeparator = '/' // OS-specific path separator
PathListSeparator = ':' // OS-specific path list separator
)
. . .
这一段为 Go 为支持的所有类 Unix 系统定义了 PathSeparator
。 注意顶部的所有构建标签,它们是与 Unix 相关的每一个可能的 Unix GOOS
平台。当GOOS
与这些名词匹配时,你的程序将产生 Unix 风格的文件路径分隔符。
按q
返回到命令行。
接下来,打开定义在 Windows 上使用filepath.Join()
时的行为的文件。
less /usr/local/go/src/os/path_windows.go
你会看到如下内容:
. . .
package os
const (
PathSeparator = '\\' // OS-specific path separator
PathListSeparator = ';' // OS-specific path list separator
)
. . .
虽然PathSeparator
的值在这里是\\
,但代码将呈现 Windows 文件路径所需的单一反斜杠(\
),因为第一个反斜杠只需要作为转义字符。
请注意,与 Unix 文件不同,它的顶部没有构建标签。这是因为GOOS
和GOARCH
可以通过在文件后缀加上分隔符和环境变量的值来作为参数传递给go build
,这个我们将会在使用 GOOS 和 GOARCH 文件后缀名做更多的研究。这里,path_windows.go
的_windows
部分使文件的行为就像它在文件的顶部有 build 标签//+build windows
。因为这个,但你程序在 windows 上运行时,它将使用path_windows.go
代码片段中的PathSeparator
和PathListSeparator
常量。
To return to the command line, quit less
by pressing q
.
要返回到命令行,按q
键退出less
。
在这一步,你建立了一个程序,展示了 Go 如何将GOOS
和GOARCH
自动转换为构建标签。考虑到这一点,你现在可以更新你的程序,编写你自己的filepath.Join()
的实现,使用构建标签为 Windows 和 Unix 平台手动设置正确的PathSeparator
。
实现一个平台特定函数
现在你已经知道 Go 的标准库是如何实现特定平台的代码的,你可以使用构建标签在你自己的应用
程序中做到这一点。要做到这一点,你将编写你自己的filepath.Join()
的实现。
打开你的main.go
文件:
nano main.go
用你自己的函数Join()
替换main.go
的内容,如下:
package main
import (
"fmt"
"strings"
)
func Join(parts ...string) string {
return strings.Join(parts, PathSeparator)
}
func main() {
s := Join("a", "b", "c")
fmt.Println(s)
}
Join 函数接收若干parts
,并使用strings 包中的strings.Join()方法将它们连接起来,使用PathSeparator
将各部分连接起来。
你还没有定义PathSeparator
,所以现在在另一个文件中做。保存并退出main.go
,打开你喜欢的编辑器,创建一个名为path.go
的新文件。
nano path.go
定义PathSeparator
,并将其设置为 Unix 文件路径分隔符,/
。
package main
const PathSeparator = "/"
编译并运行该应用程序:
go build
./app
你将会收到如下输出:
Output
a/b/c
这样运行成功,得到一个 Unix 风格的文件路径。但这还不是我们想要的:无论在什么平台上运行,输出总是 a/b/c。为了添加创建 Windows 风格文件路径的功能,你需要添加一个 Windows 版本的PathSeparator
,并告诉go build
命令使用哪个版本。在下一节中,你将使用构建标签来完成这个任务。
使用GOOS
或GOARCH
构建标签
为了考虑到 Windows 平台,你现在将创建一个替代文件到path.go
,并使用构建标签来确保代码片段只在GOOS
和GOARCH
是合适的平台时运行。
但首先,在path.go
中添加一个构建标签,告诉它除 Windows 之外的所有东西都可以进行构建。打开该文件:
nano path.go
加入如下高亮构建标签到文件:
// +build !windows
package main
const PathSeparator = "/"
Go 构建标签允许反转,也就是说,你可以指示 Go 为除 Windows 之外的任何平台构建此文件。 要反转一个构建标签,请在标签前加上一个!
。
保存并退出文件。
现在,如果你要在 Windows 上运行这个程序,你会得到以下错误:
Output
./main.go:9:29: undefined: PathSeparator
在这种情况下,Go 将无法通过引入path.go
来定义变量PathSeparator
。
现在你已经确保当GOOS
是 Windows 时,path.go
不会运行,添加一个新的文件,windows.go
:
nano windows.go
在windows.go
中,定义 Windows 的PathSeparator
,以及一个构建标签让go build
命令知道它是 Windows 的实现:
// +build windows
package main
const PathSeparator = "\\"
保存文件并从文本编辑器中退出。该应用程序现在可以以一种方式为 Windows 编译,另一种方式为所有其他平台编译。
虽然现在二进制文件可以在其他平台正确编译,但你必须做进一步的修改,以便为你无法访问的平台进行编译。要做到这一点,你将在下一步改变你的本地GOOS
和GOARCH
环境变量。
使用你本地GOOS
和GOARCH
环境变量
在前面,你通过执行go env GOOS GOACH
指令来找到你正在工作的平台是哪个操作系统和架构。当你执行go env
指令时,它会去找 2 个环境变量GOOS
和GOARCH
;如果找到,他们就会使用环境变量,如果找不到,Go 就会用当前平台的信息来设置它们。这意味着你可以改变GOOS
或GOARCH
,所以它们不是根据你的操作系统和架构默认设置的。
go build
命令的行为方式与go env
命令类似。你可以设置GOOS
或GOARCH
的环境变量,用go build
为不同的平台进行编译。
如果你没有使用Windows
系统,可以在运行go build
命令时将GOOS
环境变量设置为window
s,从而构建应用程序
windows 下的二进制版本:
GOOS=windows go build
现在列出你当前目录中的文件:
ls
列出目录文件的输出项显示在项目目录中现在有一个app.exe
的 Windows 可执行文件:
Output
app app.exe main.go path.go windows.go
使用file
命令,你可以得到关于这个文件的更多信息,确认它的构建构建信息:
file app.exe
你将会看到如下信息:
Output
app.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
你也可以在构建时设置一个,或两个环境变量。运行如下命令:
GOOS=linux GOARCH=ppc64 go build
你的应用程序
的可执行文件现在将被一个不同架构的文件所取代。在这个二进制文件上运行file
命令:
file app
你将会收到类似如下的信息:
app: ELF 64-bit MSB executable, 64-bit PowerPC or cisco 7500, version 1 (SYSV), statically linked, not stripped
通过设置本地的 GOOS
和 GOARCH
环境变量,你将可以为任何兼容 Go 的平台构建二进制文件,而无需复杂的配置或设置。接下来,你将使用文件名的约定来保持你的文件整齐,并自动为特定平台构建,而不需要构建标签。
使用GOOS
和GOARCH
文件名后缀
正如你之前看到的,Go 标准库大量使用构建标签,通过将不同的平台实现分离到不同的文件中来简化代码。当你打开os/path_unix.go
文件时,有一个构建标签,列出了所有被认为是类 Unix 平台的可能组合。然而,os/path_windows.go
文件不包含任何构建标签,因为文件名的后缀就足以告诉 Go 该文件是为哪个平台准备的。
让我们来看看这个功能的语法。命名.go
文件时,可以在文件名中按顺序添加GOOS
和GOARCH
作为后缀,用下划线(_
)来分开这两个值。如果你有一个名为filename.go
的 Go 文件,你可以通过将文件名改为 filename_GOOS_GOARCH.go
来指定操作系统和架构。举个例子,如果你希望将其编译为 64 位ARM 架构的 Windows,你会将文件名定为filename_windows_arm64.go
。这种命名方式有助于保持代码的整齐性。
使用文件名后缀而非构建标签来更新你的程序。首先,重新命名path.go
和windows.go
文件,使用os
包中使用的惯例:
mv path.go path_unix.go
mv windows.go path_windows.go
改变了这两个文件名后,你可以删除你添加到path_windows.go
的构建标签:
nano path_windows.go
移除// +build windows
,所以你的文件会看起来像这样:
package main
const PathSeparator = "\\"
保存并退出文件。
因为unix
不是一个有效的GOOS
,_unix.go
后缀对 Go 编译器没有任何意义。然而,它确实传达了文件的预期目的。 和os/path_unix.go
文件一样,你的path_unix.go
文件仍然需要使用构建标签,所以保持该文件不变。
通过使用文件名惯例,你从你的源代码中删除了不需要的构建标签,并使文件系统更干净、更清晰。
总结
为多个平台生成不需要依赖的二进制文件的能力是 Go 工具链的一个强大功能。在本教程中,你通过添加构建标签和文件名后缀来使用这种能力,以标记某些代码片段,使其只针对某些架构进行编译。你创建了你自己的平台依赖的程序,然后操纵GOOS
和GOARCH
环境变量,为你当前平台以外的平台生成二进制文件。这是一项有价值的技能,因为有一个持续集成过程,自动运行这些环境变量,为所有平台构建二进制文件,这是一个常见的做法。