在不同的操作系统和架构编译 Go 应用

在软件开发中,重要的是要考虑你想为之编译二进制的操作系统和底层处理器架构。因为在不同的操作系统/架构平台上运行一个二进制文件通常很慢或不可能,所以通常的做法是为许多不同的平台编译你最终的二进制文件,以最大化你的程序的受众。然而,这通常是很困难的,当你开发软件的平台和你想要部署的平台不是同一个的时候。例如,在过去,在 Windows 上开发一个程序并将其部署到 Linux 或 macOS 机器上,需要为每一个你想要的二进制文件的环境设置构建机器。你还需要保持你的工具同步,此外还有其他考虑因素,这些因素会增加成本,使协作测试和分布式更加困难。

Go 通过在go build工具中直接建立对多平台的支持,以及 Go 工具链的其他部分解决了这个问题。通过使用环境变量构建标签,你可以控制你最终的二进制文件是为哪个操作系统和架构构建的,此外还可以把一个工作流程放在一起,在不改变你的代码库的情况下快速切换对平台依赖的代码。

在本教程中,你将把一个将strings连接成文件路径的示例应用程序放在一起,创建并有选择地包括与平台有关的片段,并在你自己的系统上为多个操作系统和系统架构构建二进制文件,向你展示如何使用 Go 编程语言的这一强大能力。

前期准备

为了跟随本文的例子,你将需要:

GOOSGOARCH可能支持的平台

在展示如何控制构建过程为不同的平台构建二进制文件之前,让我们先了解一下 Go 能够为哪些类型的平台进行构建,以及 Go 如何使用环境变量GOOSGOARCH关联这些平台。

Go 工具有一个命令,可以打印出 Go 可以构建的平台的列表。这个列表会随着每一个新的 Go 版本而改变,所以这里讨论的组合在另一个版本的 Go 中可能不一样。当下写这个教程的时候,Go release 版本是 1.13.

为了找到适用的平台,执行如下命令:

  1. go tool dist list

你将会收到如下相似的输出:

  1. Output
  2. aix/ppc64 freebsd/amd64 linux/mipsle openbsd/386
  3. android/386 freebsd/arm linux/ppc64 openbsd/amd64
  4. android/amd64 illumos/amd64 linux/ppc64le openbsd/arm
  5. android/arm js/wasm linux/s390x openbsd/arm64
  6. android/arm64 linux/386 nacl/386 plan9/386
  7. darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64
  8. darwin/amd64 linux/arm nacl/arm plan9/arm
  9. darwin/arm linux/arm64 netbsd/386 solaris/amd64
  10. darwin/arm64 linux/mips netbsd/amd64 windows/386
  11. dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64
  12. 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 , windowsdarwin作为 GOOS 的值。这些涵盖了三大操作系统平台: LinuxWindowsmacOS,后者是基于Darwin Operating system的,因此被称为darwin。然而,Go 也可以覆盖不太主流的平台,如nacl,它代表了谷歌的本地客户端

当你运行go build这样的命令时,Go 使用当前平台的GOOSGOARCH来决定如何构建二进制文件。要想知道你的平台是什么组合,你可以使用go env命令,并将GOOSGOARCH作为参数:

  1. go env GOOS GOARCH

在测试这个例子时,我们在一台AMD64 架构的机器上的 macOS 上运行这个命令,所以我们将收到以下输出:

  1. Output
  2. darwin
  3. amd64

这个命令的输出告诉我们系统的 GOOS 是 darwin,GOARCH 是 amd64。

你现在知道了 Go 中的GOOSGOARCH是什么,以及它们的可能值。接下来,你将编写一个程序,作为如何使用这些环境变量和构建标签为其他平台构建二进制文件的例子。

filepath.Join编写一个平台依赖的应用程序

在你开始构建其他平台的二进制前,让我们先构建一个范例程序。出于这个目的,一个好的例子可以用 Go 标准库中的path/filepath包内的Join函数。这个函数以多个 string 为传参,并返回一个用正确文件路径分隔符拼接的 string。

这是一个很好的范例程序,因为该程序的运行取决于它在哪个操作系统上运行。在 Windows 上,路径分隔符是反斜杠,\,而基于 Unix 的系统使用正斜杠,/

让我们从构建一个使用filepath.Join()的应用程序开始,稍后,你将编写你自己的Join()函数的实现,将代码定制为特定平台的二进制文件。

首先,在你的src目录下创建一个文件夹,用你的应用程序的名字命名:

  1. mkdir app

进入目录:

  1. cd app

接下来,在你选择的文本编辑器中创建一个名为main.go的新文件。在本教程中,我们将使用 Nano。

  1. nano main.go

文件打开后,添加如下代码:

  1. package main
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. )
  6. func main() {
  7. s := filepath.Join("a", "b", "c")
  8. fmt.Println(s)
  9. }

在这个文件的main()函数用filepath.Join()将三个strings用正确的,平台依赖的路径分隔符连接起来。

保存并退出文件,然后运行程序:

  1. go run main.go

当运行这个程序时,你将收到不同的输出,这取决于你所使用的平台。在 Windows 上,你会看到用\分隔的字符串。

  1. Output
  2. a\b\c

在 MacOS 和 Linux 等 Unix 系统上,你将收到以下内容。

  1. Output
  2. a/b/c

这表明,由于这些操作系统使用的文件系统协议不同,程序将不得不为不同的平台构建不同的代码。但由于它已经根据操作系统使用了不同的文件分隔符,所有我们知道filepath.Join()已经考虑了平台的差异。这是因为 Go 工具链会自动检测你的机器的GOOSGOARCH,并使用这些信息来使用具有正确构建标签和文件分隔符的代码片段。

让我们思考一下filepath.Join()函数的分隔符是从哪里来的。运行以下命令来查看 Go 标准库中的相关片段:

  1. less /usr/local/go/src/os/path_unix.go

这将显示path_unix.go的内容。寻找该文件的如下部分:

  1. . . .
  2. // +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris
  3. package os
  4. const (
  5. PathSeparator = '/' // OS-specific path separator
  6. PathListSeparator = ':' // OS-specific path list separator
  7. )
  8. . . .

这一段为 Go 为支持的所有类 Unix 系统定义了 PathSeparator。 注意顶部的所有构建标签,它们是与 Unix 相关的每一个可能的 Unix GOOS平台。当GOOS与这些名词匹配时,你的程序将产生 Unix 风格的文件路径分隔符。

q返回到命令行。

接下来,打开定义在 Windows 上使用filepath.Join()时的行为的文件。

  1. less /usr/local/go/src/os/path_windows.go

你会看到如下内容:

  1. . . .
  2. package os
  3. const (
  4. PathSeparator = '\\' // OS-specific path separator
  5. PathListSeparator = ';' // OS-specific path list separator
  6. )
  7. . . .

虽然PathSeparator的值在这里是\\,但代码将呈现 Windows 文件路径所需的单一反斜杠(\),因为第一个反斜杠只需要作为转义字符。

请注意,与 Unix 文件不同,它的顶部没有构建标签。这是因为GOOSGOARCH可以通过在文件后缀加上分隔符和环境变量的值来作为参数传递给go build,这个我们将会在使用 GOOS 和 GOARCH 文件后缀名做更多的研究。这里,path_windows.go_windows部分使文件的行为就像它在文件的顶部有 build 标签//+build windows。因为这个,但你程序在 windows 上运行时,它将使用path_windows.go代码片段中的PathSeparatorPathListSeparator常量。

To return to the command line, quit less by pressing q.

要返回到命令行,按q键退出less

在这一步,你建立了一个程序,展示了 Go 如何将GOOSGOARCH自动转换为构建标签。考虑到这一点,你现在可以更新你的程序,编写你自己的filepath.Join()的实现,使用构建标签为 Windows 和 Unix 平台手动设置正确的PathSeparator

实现一个平台特定函数

现在你已经知道 Go 的标准库是如何实现特定平台的代码的,你可以使用构建标签在你自己的应用程序中做到这一点。要做到这一点,你将编写你自己的filepath.Join()的实现。

打开你的main.go文件:

  1. nano main.go

用你自己的函数Join()替换main.go的内容,如下:

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. func Join(parts ...string) string {
  7. return strings.Join(parts, PathSeparator)
  8. }
  9. func main() {
  10. s := Join("a", "b", "c")
  11. fmt.Println(s)
  12. }

Join 函数接收若干parts,并使用strings 包中的strings.Join()方法将它们连接起来,使用PathSeparator将各部分连接起来。

你还没有定义PathSeparator,所以现在在另一个文件中做。保存并退出main.go,打开你喜欢的编辑器,创建一个名为path.go的新文件。

  1. nano path.go

定义PathSeparator,并将其设置为 Unix 文件路径分隔符,/

  1. package main
  2. const PathSeparator = "/"

编译并运行该应用程序:

  1. go build
  2. ./app

你将会收到如下输出:

  1. Output
  2. a/b/c

这样运行成功,得到一个 Unix 风格的文件路径。但这还不是我们想要的:无论在什么平台上运行,输出总是 a/b/c。为了添加创建 Windows 风格文件路径的功能,你需要添加一个 Windows 版本的PathSeparator,并告诉go build命令使用哪个版本。在下一节中,你将使用构建标签来完成这个任务。

使用GOOSGOARCH构建标签

为了考虑到 Windows 平台,你现在将创建一个替代文件到path.go,并使用构建标签来确保代码片段只在GOOSGOARCH是合适的平台时运行。

但首先,在path.go中添加一个构建标签,告诉它除 Windows 之外的所有东西都可以进行构建。打开该文件:

  1. nano path.go

加入如下高亮构建标签到文件:

  1. // +build !windows
  2. package main
  3. const PathSeparator = "/"

Go 构建标签允许反转,也就是说,你可以指示 Go 为除 Windows 之外的任何平台构建此文件。 要反转一个构建标签,请在标签前加上一个!

保存并退出文件。

现在,如果你要在 Windows 上运行这个程序,你会得到以下错误:

  1. Output
  2. ./main.go:9:29: undefined: PathSeparator

在这种情况下,Go 将无法通过引入path.go来定义变量PathSeparator

现在你已经确保当GOOS是 Windows 时,path.go不会运行,添加一个新的文件,windows.go

  1. nano windows.go

windows.go中,定义 Windows 的PathSeparator,以及一个构建标签让go build命令知道它是 Windows 的实现:

  1. // +build windows
  2. package main
  3. const PathSeparator = "\\"

保存文件并从文本编辑器中退出。该应用程序现在可以以一种方式为 Windows 编译,另一种方式为所有其他平台编译。

虽然现在二进制文件可以在其他平台正确编译,但你必须做进一步的修改,以便为你无法访问的平台进行编译。要做到这一点,你将在下一步改变你的本地GOOSGOARCH环境变量。

使用你本地GOOSGOARCH环境变量

在前面,你通过执行go env GOOS GOACH指令来找到你正在工作的平台是哪个操作系统和架构。当你执行go env指令时,它会去找 2 个环境变量GOOSGOARCH;如果找到,他们就会使用环境变量,如果找不到,Go 就会用当前平台的信息来设置它们。这意味着你可以改变GOOSGOARCH,所以它们不是根据你的操作系统和架构默认设置的。

go build命令的行为方式与go env命令类似。你可以设置GOOSGOARCH的环境变量,用go build为不同的平台进行编译。

如果你没有使用Windows系统,可以在运行go build命令时将GOOS环境变量设置为windows,从而构建应用程序windows 下的二进制版本:

  1. GOOS=windows go build

现在列出你当前目录中的文件:

  1. ls

列出目录文件的输出项显示在项目目录中现在有一个app.exe的 Windows 可执行文件:

  1. Output
  2. app app.exe main.go path.go windows.go

使用file命令,你可以得到关于这个文件的更多信息,确认它的构建构建信息:

  1. file app.exe

你将会看到如下信息:

  1. Output
  2. app.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows

你也可以在构建时设置一个,或两个环境变量。运行如下命令:

  1. GOOS=linux GOARCH=ppc64 go build

你的应用程序的可执行文件现在将被一个不同架构的文件所取代。在这个二进制文件上运行file命令:

  1. file app

你将会收到类似如下的信息:

  1. app: ELF 64-bit MSB executable, 64-bit PowerPC or cisco 7500, version 1 (SYSV), statically linked, not stripped

通过设置本地的 GOOSGOARCH 环境变量,你将可以为任何兼容 Go 的平台构建二进制文件,而无需复杂的配置或设置。接下来,你将使用文件名的约定来保持你的文件整齐,并自动为特定平台构建,而不需要构建标签。

使用GOOSGOARCH文件名后缀

正如你之前看到的,Go 标准库大量使用构建标签,通过将不同的平台实现分离到不同的文件中来简化代码。当你打开os/path_unix.go文件时,有一个构建标签,列出了所有被认为是类 Unix 平台的可能组合。然而,os/path_windows.go文件不包含任何构建标签,因为文件名的后缀就足以告诉 Go 该文件是为哪个平台准备的。

让我们来看看这个功能的语法。命名.go文件时,可以在文件名中按顺序添加GOOSGOARCH作为后缀,用下划线(_)来分开这两个值。如果你有一个名为filename.go的 Go 文件,你可以通过将文件名改为 filename_GOOS_GOARCH.go来指定操作系统和架构。举个例子,如果你希望将其编译为 64 位ARM 架构的 Windows,你会将文件名定为filename_windows_arm64.go。这种命名方式有助于保持代码的整齐性。

使用文件名后缀而非构建标签来更新你的程序。首先,重新命名path.gowindows.go文件,使用os包中使用的惯例:

  1. mv path.go path_unix.go
  2. mv windows.go path_windows.go

改变了这两个文件名后,你可以删除你添加到path_windows.go的构建标签:

  1. nano path_windows.go

移除// +build windows,所以你的文件会看起来像这样:

  1. package main
  2. const PathSeparator = "\\"

保存并退出文件。

因为unix不是一个有效的GOOS_unix.go后缀对 Go 编译器没有任何意义。然而,它确实传达了文件的预期目的。 和os/path_unix.go文件一样,你的path_unix.go文件仍然需要使用构建标签,所以保持该文件不变。

通过使用文件名惯例,你从你的源代码中删除了不需要的构建标签,并使文件系统更干净、更清晰。

总结

为多个平台生成不需要依赖的二进制文件的能力是 Go 工具链的一个强大功能。在本教程中,你通过添加构建标签和文件名后缀来使用这种能力,以标记某些代码片段,使其只针对某些架构进行编译。你创建了你自己的平台依赖的程序,然后操纵GOOSGOARCH环境变量,为你当前平台以外的平台生成二进制文件。这是一项有价值的技能,因为有一个持续集成过程,自动运行这些环境变量,为所有平台构建二进制文件,这是一个常见的做法。