Golang 交叉编译和条件编译详解

前言

通常情况下某平台编译的代码只有该平台能够运行访问,若拷贝到其他平台可能会因为无法识别文件格式而无法运行。欲使代码能够在目标平台运行,只能专门编写面向目标平台的代码并在该平台编译,这虽然能够实现目的,但毫无疑问跨平台性肯定是非常差的。交叉编译是直接在一个平台上生成另一个平台上的可执行代码,比如在 Windows 平台上开发的程序可以编译生成 Linux 和 MAC 平台的可执行文件。Java 是通过 JVM 来提供跨平台的支持,但 JVM 的臃肿会在一定程度上增添性能损耗,而 Golang 提供了更为方便简洁的交叉编译功能。

交叉编译

参数说明

Golang 的交叉编译主要通过以下几个参数实现:

  1. CGO_ENABLED

CGO_ENABLED 用来控制 golang 编译期间是否支持调用 cgo 命令的开关,其值为 1 或 0 ,默认情况下值为 1,可以用 go env 查看默认值。通常该值不影响简单程序的编译,但当你的程序里调用了cgo 命令,则此参数必须设置为 1 ,否则将编译时出错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
//   printf("%sn", s);
// }
import "C"
import "unsafe"

func main() {
    cs := C.CString("Hello from stdio")
    C.myprint(cs)
    C.free(unsafe.Pointer(cs))
}

CGO_ENABLED=1 时,上述代码可以正常编译运行:

1
2
(base) PS D:\CommonProject\GoTest> go env -w CGO_ENABLED=1 && go run main.go
Hello from stdion

CGO_ENABLED=0 时,上述代码则编译失败:

1
2
(base) PS D:\CommonProject\GoTest> go env -w CGO_ENABLED=0 && go run main.go
go: no Go source files

很多项目里编译的时候都明确指定了此环境变量的值,主要是编译器在编译时会根据不同的情况使用不同的编译方法。当指定 CGO_ENABLED=1 编译时, 会将文件中引用 libc 的库(比如常用的 net 包),以动态链接的方式生成目标文件。当指定 CGO_ENABLED=0 编译时,则会把在目标文件中未定义的符号(外部函数)一起链接到可执行文件中。但不论哪种方式,都可以使用静态连接编译

  1. GOOS

GOOS 用于指定程序构建环境的目标操作系统,主要有 darwin 、 freebsd 、 linux 、 windows 等。

  1. GOARCH

GOARCH 用于指定程序构建环境的目标计算架构,主要有 386、amd64、arm 等,若不设置,默认值与程序编译环境的计算架构一致

示例

对于上述示例程序,可分别通过不同命令来编译生成不同平台下的可执行文件,在 Mac 下编译 Linux 、 Windows 平台的 64 位可执行程序:

1
2
CGO_ENABLED=1  GOOS=linux    GOARCH=amd64 go build main.go
CGO_ENABLED=1  GOOS=windows  GOARCH=amd64 go build main.go

在 Linux 下编译 Mac、 Windows 平台的 64 位可执行程序:

1
2
CGO_ENABLED=1  GOOS=darwin   GOARCH=amd64 go build main.go
CGO_ENABLED=1  GOOS=windows  GOARCH=amd64 go build main.go

在 Windows 下编译 Mac 、 Linux 平台的 64 位可执行程序:

1
2
3
4
5
6
7
8
9
SET CGO_ENABLED=1
SET GOOS=darwin3
SET GOARCH=amd64
go build main.go

SET CGO_ENABLED=1
SET GOOS=linux
SET GOARCH=amd64
go build main.go

Windows下如果将命令放在一行通过 && 连接执行,可能会出现错误:

1
cmd/go: unsupported GOOS/GOARCH pair linux /amd64

原因是 SET 命令会将 GOOS=linux 后的空格也赋值给了 GOOS 从而导致 Golang 无法识别,分为多行执行则不会出现该问题。交叉编译程序是否是按照预期进行编译,可以通过go list命令进行验证:

1
2
3
# linux 
$: GOOS=linux go list -f '{{.GoFiles}}' os/exec
[exec.go exec_unix.go lp_unix.go]

条件编译

条件编译解决的是一份代码在不同的编译平台以及不同的语言版本的兼容性问题,即实现一份代码处处都可以编译。Go 语言中的条件编译主要可以分为文件名后缀方式和编译标签方式两种方式。

文件名后缀方式

go build 命令在不读取源文件的情况下可以通过文件名后缀以决定某个文件是否需要参与编译,文件名后缀的形式主要有:

1
2
3
_$GOOS.go
_$GOARCH.go
_$GOOS_$GOARCH.go

同时指定 GOOSGOARCH 时,需保持 GOOS 在前而不能颠倒。

编译标签方式

编译标签方式是更加灵活的条件编译方式,它通过在文件头增加条件编译标签来告诉编译器本文件是否参与编译。条件编译的标签格式以 // +build 前缀开始,且其后必须至少为一个空行,否则编译器就无法识别。编译标签中具体条件组合的规则有:

  • 空格 ' ' = OR
  • 逗号 ',' = AND
  • 感叹号 '!' = NOT
  • 换行 '\n'= OR

示例 1:

1
// +build linux,386 darwin,!cgo

其含义为:

1
(linux AND 386) OR (darwin AND (NOT cgo))

示例 2:

1
2
// +build linux darwin
// +build 386

其含义为:

1
(linux OR darwin) AND 386

编译标签支持的条件有如下几种:

  • 操作系统, 值可以通过 runtime.GOOS 获取
  • CPU架构, 值可以通过 runtime.GOARCH 获取
  • 编译器,如 gc, gccgo
  • 是否开启 Cgo, cgo
  • 语言版本, Go版本如 go1.1,…,go1.12

编译标签除了上述用法外,还支持自定义约束功能,需配合 go build-tags 参数使用。例如源文件中包含自定义编译标签:

1
// +build beta,debug

在编译时则存在匹配关系如下:

1
2
3
go build -tags "debug beta" // 匹配成功
go build -tags "debug"      // 匹配失败
go build -tags "debug \!cgo"    // 匹配失败

参考