0%

一文搞懂 Go 跨平台编译 - 交叉编译

在实际的工作中,我们很多时候开发环境跟应用程序最终运行的环境是不同的操作系统,比如在 Windows 上进行开发,但是应用程序最终是要在 Linux 上运行的, 又或者是在 mac 下开发,在 Linux 下运行。这个时候我们就需要进行交叉编译,即在一个操作系统上编译出另一个操作系统的可执行文件。

使用 Go 的时候,我们可以很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可,本文会通过一个简单的例子来演示如何进行交叉编译。

GOOS 和 GOARCH 环境变量所有可能的值

在 Go 语言中,我们可以通过设置环境变量 GOOSGOARCH 来指定目标操作系统和目标架构。 比如在我的系统上,查看 GOOSGOARCH 的值:

1
2
3
➜ go env GOOS GOARCH
darwin
amd64

在 Go 编译的时候,默认的 GOOSGOARCH 的值是当前系统的操作系统和架构,比如在我的系统上,GOOS 的值是 darwinGOARCH 的值是 amd64。 所以编译出来的就是当前系统可以执行的二进制文件,如果我们想要编译出其他系统的二进制文件,就需要设置 GOOSGOARCH 的值。

首先,我们需要了解这两个环境变量支持哪些值。下面是所有可能的值,我们可以通过 go tool dist list 列出来:

1
2
3
4
5
6
7
8
9
10
11
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

在上面的输出中,/ 前面操作系统,/ 后面是架构。以 linux/386 为例,键值对以 GOOS 开始,在本例中将是 linux ,指的是 Linux 操作系统。这里的 GOARCH 将是 386 ,代表 Intel 80386 微处理器。

我们发现其实 Go 支持很多操作系统和架构,但是大多数情况下,你最终会使用 linuxwindowsdarwin 中的一个作为 GOOS 的值,这涵盖了三大操作系统平台:Linux、Windows 和 macOS。

使用文件名后缀实现交叉编译

使用场景:不同操作系统需要通过不同代码来实现。

Go 标准库中 path/filepath 包中的 Join 函数,在不同平台下会有不同的效果。该函数接受一些字符串,并返回一个使用正确文件路径分隔符连接在一起的字符串。

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

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"path/filepath"
)

func main() {
s := filepath.Join("a", "b", "c")
fmt.Println(s)
}

这个程序在 Windows 上运行时,将输出 a\b\c,而在 Unix 系统上运行时,将输出 a/b/c

这是如何实现的呢?这就涉及到了 Go 中实现交叉编译的其中一种方式,就是指定文件名后缀, 我们看 Go 的源码或者一些开源项目的源码,就会发现有些文件的文件名带了操作系统的后缀,比如 file_windows.gofile_linux.gofile_darwin.go 等等。

同样的,path/filepath 包中的 Join 函数也是这样实现的,我们可以看到 path/filepath 包中有很多文件,比如 path_windows.gopath_unix.go 等等,其中:

  • path_windows.go 中实现了 Join 函数在 Windows 上的实现
  • path_unix.go 中实现了 Join 函数在 Unix 系统上的实现

我们点开 path_unix.go 文件,可以看到如下的代码:

1
2
3
4
5
// ...
const (
PathSeparator = '/' // OS-specific path separator
PathListSeparator = ':' // OS-specific path list separator
)

也就是说,Join 函数的路径分隔符是在这里通过 PathSeparator 定义成 / 的,而在 path_windows.go 文件中,PathSeparator 是定义成 \ 的。

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

有两个 \ 是因为需要转义。

同时在文件名中加上 GOARCH 后缀

在命名文件时,您可以按照顺序将 GOOSGOARCH 添加为文件名的后缀,用下划线(_)分隔这些值。如果您有一个名为 filename.go 的 Go 文件,您可以通过将文件名更改为 filename_GOOS_GOARCH.go 来指定操作系统和架构。例如,如果您希望将其编译为具有 64 位 ARM 架构的 Windows 文件,您将文件名更改为 filename_windows_arm64.go 。这种命名约定有助于保持代码整洁有序。

在我们编译的时候,如果我们当前的 GOOSGOARCH 跟文件名不匹配,则 Go 会忽略这个文件。

使用构建标签实现交叉编译

使用场景:不同操作系统需要使用不同代码。(跟上一个类似)

除了指定文件名后缀以外,我们还可以使用构建标签来实现交叉编译。具体来说,就是在文件的第一行添加 // +build 标签,比如:

1
2
3
4
5
// +build windows

package main

const PathSeparator = "\\"

这样的话,这个文件就只会在 Windows 上编译,而在其他系统上不会编译。

使用你本地 GOOS 和 GOARCH 的值进行交叉编译

使用场景:在本地开发环境编译出其他系统的可执行文件。

之前,您运行了 go env GOOS GOARCH 命令来查看您正在使用的操作系统和架构。当您运行 go env 命令时,它会查找两个环境变量 GOOSGOARCH;如果找到,它们的值将被使用,但如果未找到,则 Go 将使用当前平台的信息来设置它们。这意味着您可以更改 GOOSGOARCH,以便它们不会默认为您的本地操作系统和架构。这样就可以编译出其他平台的可执行文件。

go build 命令的行为方式类似于 go env 命令。您可以使用 go build 设置 GOOSGOARCH 环境变量以构建不同平台的应用程序。

如果您没有使用 Windows 系统,请在运行 go build 命令时将 GOOS 环境变量设置为 windows

1
GOOS=windows go build

你也可以同时设置 GOARCH 环境变量:

1
GOOS=linux GOARCH=amd64 go build

这将编译出一个 Linux 平台上的 64 位可执行文件,我们如果使用的是 macOS,我们可以通过 file 命令查看编译出来的文件的信息:

1
2
file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=xx, with debug_info, not stripped

我们会看到这是一个 64 位的 ELF 可执行文件,而 ELF 是 Linux 下的可执行文件格式。

更加现代化的交叉编译方式

我们前面讲了很多如何进行交叉编译,但是如果我们每次都需要针对不同平台来手动编译,未免过于麻烦,当然我们可以写一个脚本来自动化这个过程。

这一小节,我将介绍一个比较好用的交叉编译工具 goreleaser,我们只需要简单的配置一下,它就可以帮我们自动化交叉编译的过程。 比如 frp 这个开源项目就是使用 goreleaser 来进行发布新版本的。

下面是一个示例配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# .goreleaser.yml,放在项目根目录下
# 项目名称
project_name: goss
# 在执行前需要执行的命令
before:
hooks:
- go mod tidy
# 编译配置
builds:
- env:
# 可以指定环境变量
- CGO_ENABLED=0
goos: # 需要编译的操作系统
- linux
- windows
- darwin
archives:
- replacements: # 将 GOARCH 替换,因为用户更熟悉 x86_64
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

说明:

  • project_name 是项目的名称
  • before 是在执行前需要执行的命令
  • builds 是编译配置,env 是环境变量,goos 是需要编译的操作系统
  • archives 是归档配置,replacements 是将 GOARCH 替换。
  • checksum 是生成 checksum 的配置

接着我们只需要执行 goreleaser build 命令即可进行编译:

goreleaser 的安装方式可参考它的官网。

1
goreleaser build

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
• starting build...
• loading config file file=.goreleaser.yaml
• loading environment variables
• getting and validating git state
• building... commit=e913b9258e649f8f2784d9daaebbf3a4d7cf7a17 latest tag=v0.0.9
• parsing tag
• setting defaults
• running before hooks
• running hook=go mod tidy
• running hook=go generate ./...
• checking distribution directory
• loading go mod information
• build prerequisites
• writing effective config file
• writing config=dist/config.yaml
• generating changelog
• writing changelog=dist/CHANGELOG.md
• building binaries
• building binary=dist/goss_windows_amd64_v1/goss.exe
• building binary=dist/goss_darwin_arm64/goss
• building binary=dist/goss_windows_arm64/goss.exe
• building binary=dist/goss_darwin_amd64_v1/goss
• building binary=dist/goss_linux_amd64_v1/goss
• building binary=dist/goss_linux_arm64/goss
• building binary=dist/goss_windows_386/goss.exe
• building binary=dist/goss_linux_386/goss
• took: 39s
• storing release metadata
• writing file=dist/artifacts.json
• writing file=dist/metadata.json
• build succeeded after 39s

编译完成后,我们会在 dist 目录下看到编译好的文件:

1
ls -l dist/

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
total 32
-rw-r--r-- 1 ruby staff 36 Feb 3 11:12 CHANGELOG.md
-rw-r--r-- 1 ruby staff 1803 Feb 3 11:13 artifacts.json
-rw-r--r-- 1 ruby staff 3509 Feb 3 11:12 config.yaml
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_darwin_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_darwin_arm64
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_386
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_arm64
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_386
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_arm64
-rw-r--r-- 1 ruby staff 219 Feb 3 11:13 metadata.json

接着,我们就可以来发布这些二进制文件了。如果我们有其他个性化的需求,我们可以通过修改 .goreleaser.yml 文件来满足我们的需求。它还有很多配置可以自定义。 如果后续我们需要调整,只需要修改一下配置文件就行了,比如我们需要支持一个新的操作系统,只需要在 goos 下面增加一个新的操作系统即可。

使用 goreleaser 进行交叉编译的好处是,它会自动帮我们打包、生成 checksum、生成 changelog 等等,省去了很多手动操作。

另外,它还支持直接发布到 Github,使用 Github Actions 来自动化这个过程,这样我们只需要 push 代码,就可以自动进行编译、打包、发布。

下面是一个 github workflow 的示例配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
name: goreleaser

on:
push:
# run only against tags
tags:
- '*'

permissions:
contents: write
# packages: write
# issues: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Fetch all tags
run: git fetch --force --tags
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

这个配置文件的意思是,当我们 push tag 的时候,就会触发这个 workflow,它会自动运行 goreleaser,然后进行编译、打包、发布。

总结

Go 支持我们很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可:

  • 环境变量:GOOSGOARCH
  • 文件名后缀:filename_GOOS_GOARCH.go
  • 构建标签:// +build 标签

另外,我们还可以使用 goreleaser 这个工具来自动化交叉编译的过程,它还支持直接发布到 Github,使用 Github Actions 来自动化这个过程,这样我们只需要 push tag,就可以自动进行编译、打包、发布。