Go语言如何使用条件编译
[译] Go语言如何使用条件编译
当开发需要依赖底层平台或处理器体系特性的Go包时,提供对应的特定实现是非常有必要的。
Go没有预处理,没有宏定义系统,不可以像c语言那样使用#define
来控制是否包含平台相关的特定代码。作为替代,Go使用go/build包中定义的标签系统(system of tags
)和命名约定(naming convention
)以及go tool
中的相应支持来允许Go包编译特定代码。
这篇文章说明条件编译是如何实现的以及如何在你自己的工程中使用它。
先说go list
讨论条件编译之前,我们需要简单讨论下go list
命令。go list
允许你访问包内部的数据结构从而驱动编译过程(build process)。
go list
的大部分参数和go build
、go test
、go install
相同,但是它不会执行编译。使用-f
格式化参数,我们可以填入一段text/template
的模板代码,它会在一个包含go/build.Package结构的上下文环境被执行。 如下例子,可以得到所有将被编译的源码文件名。
1 | % go list -f '{{.GoFiles}}' os/exec |
这个例子查询了在我当前执行环境linux/arm
下os/exec
包中将会被编译的文件。结果文件有两个:exec.go
包含了在所有平台共享的通用代码,lp_unix.go
包含了一份在unix-like
系统独有的exec.LookPath
实现。
如果我在Windows系统执行上面那个命令,结果如下:
1 | C:\go> go list -f '{{.GoFiles}}' os/exec |
这个简短的例子演示了Go条件编译系统,被称为构建规则(Build Constraints),接下来将更详细的讨论它。
构建标签(Build tags)
第一种实现条件编译的方法是在源码中插入注释,被称之为构建标签。
构建标签的注释应该尽可能的接近源码文件的顶部位置。
当Go编译一个包时,它会分析包内的每个源码文件并查找构建标签。标签决定了这个源码文件是否被编译。
构建标签遵循以下三个原则:
- 空格隔开的选项是或(OR)的关系
- 逗号隔开的选项是与(AND)的关系
- 每个选项由字母和数字组成。如果前面加上
!
,则表示反义
1 | // +build darwin freebsd netbsd openbsd |
上面的例子,构建标签出现在某个源码文件的顶部,表示这个源码文件只会在支持kqueue
的BSD系统中被编译。
一个源码文件可以包含多个构建标签。构建规则是每个独立规则的逻辑与关系。如下例子表示该文件将在linux/386
或darwin/386
平台才会被编译。
1 | // +build linux darwin |
编译只需要在go build指令后用-tags指定编译条件即可
1 | go build -tags linux |
关于注释的补充说明
第一次使用构建标签时常见的一个错误如下:
1 | // +build !linux |
这个例子中,构建标签和包声明间没有空行。这使得构建标签被当成包声明的描述信息而被忽略。正确的格式如下:
1 | // +build !linux |
译者yoko注,原文中,这里有段描述说正确的格式执行go vet时,会报如下错误
+build comment appears too late in file
我做了简单测试,正确的格式执行go vet并不会报错,相反,错误的格式反而会报如下错误+build comment must appear before package clause and be followed by a blank line
我猜应该是新版本的Go对go vet做了改进,我测试时的环境为go version go1.12.5 darwin/amd64
以下是一个包含了许可证,构建标签,包声明的例子:
1 | % head headspin.go |
文件名后缀(File suffixes)
第二种条件编译的方法是通过源码文件的文件名实现的。这种方案比构造标签方案更简单。
go/build
包的文档有关于命名约定的描述。简单来说,如果文件名包含_$GOOS.go
后缀,那么这个源码文件只会在对应的平台被编译。其他平台会忽略这个文件。另一种约定是_$GOARCH.go
。这两种后缀可以组合起来,但要保证顺序,正确的格式是_$GOOS_$GOARCH.go
,错误的格式是_$GOARCH_$GOOS.go
。
以下是文件名后缀的一些例子:
1 | mypkg_freebsd_arm.go // 只在 freebsd/arm 系统编译 |
源码文件光有后缀是不够的,比如如下文件名:
1 | _linux.go |
即使是在linux或freebsd系统,这两个文件也会被忽略,原因是go/build
包会忽略所有文件名以.
和_
开始的文件。
使用构建标签还是文件名后缀
构建标签和文件名后缀在功能上是重叠的。比如,一个名为mypkg_linux.go
的文件,再包含构建标签// +build linux
会显得多余。
通常来说,当只有一个特定平台或体系需要指定时,我们选择文件名后缀的方式。比如:
1 | mypkg_linux.go // 只在 linux 系统编译 |
相反,如果你的文件需要指定给多个平台或体系使用,或者你需要排除某个特定平台时,我们选择构建标签的方式。比如:
1 | // 在所有类unix平台编译 |
总结
本篇文章只关注了Go源码文件,事实上,构建标签和文件名后缀可作用于任何go tool可以编译的源码文件,包括.c
和.s
文件。Go标准库中,尤其是runtime,syscall,os,net包中包含了大量这种例子。
测试文件也支持构建标签和文件名后缀,它们的表现和Go源码文件表现一致。从而允许我们为特定平台指定特定测试用例。
最后,本篇文章讨论的是go tool
,事实上,条件编译特性并不局限于这个工具。你可以通过go/build
包构建你自己的工具来分析文件名后缀以及构建标签的语法。