[译] Go语言如何使用条件编译

https://zhuanlan.zhihu.com/p/76535890

当开发需要依赖底层平台或处理器体系特性的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 buildgo testgo install相同,但是它不会执行编译。使用-f格式化参数,我们可以填入一段text/template的模板代码,它会在一个包含go/build.Package结构的上下文环境被执行。 如下例子,可以得到所有将被编译的源码文件名。

1
2
% go list -f '{{.GoFiles}}' os/exec
[exec.go lp_unix.go]

这个例子查询了在我当前执行环境linux/armos/exec包中将会被编译的文件。结果文件有两个:exec.go包含了在所有平台共享的通用代码,lp_unix.go包含了一份在unix-like系统独有的exec.LookPath实现。

如果我在Windows系统执行上面那个命令,结果如下:

1
2
C:\go> go list -f '{{.GoFiles}}' os/exec
[exec.go lp_windows.go]

这个简短的例子演示了Go条件编译系统,被称为构建规则(Build Constraints),接下来将更详细的讨论它。

构建标签(Build tags)

第一种实现条件编译的方法是在源码中插入注释,被称之为构建标签。

构建标签的注释应该尽可能的接近源码文件的顶部位置。

当Go编译一个包时,它会分析包内的每个源码文件并查找构建标签。标签决定了这个源码文件是否被编译。

构建标签遵循以下三个原则:

  1. 空格隔开的选项是或(OR)的关系
  2. 逗号隔开的选项是与(AND)的关系
  3. 每个选项由字母和数字组成。如果前面加上!,则表示反义
1
// +build darwin freebsd netbsd openbsd

上面的例子,构建标签出现在某个源码文件的顶部,表示这个源码文件只会在支持kqueue的BSD系统中被编译。

一个源码文件可以包含多个构建标签。构建规则是每个独立规则的逻辑与关系。如下例子表示该文件将在linux/386darwin/386平台才会被编译。

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

编译只需要在go build指令后用-tags指定编译条件即可

1
go build -tags linux

关于注释的补充说明

第一次使用构建标签时常见的一个错误如下:

1
2
// +build !linux
package mypkg // 错误

这个例子中,构建标签和包声明间没有空行。这使得构建标签被当成包声明的描述信息而被忽略。正确的格式如下:

1
2
3
// +build !linux

package mypkg // 正确

译者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
2
3
4
5
6
7
8
9
10
% head headspin.go
// Copyright 2013 Way out enterprises. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build someos someotheros thirdos,!amd64

// Package headspin implements calculates numbers so large
// they will make your head spin.
package headspin

文件名后缀(File suffixes)

第二种条件编译的方法是通过源码文件的文件名实现的。这种方案比构造标签方案更简单。

go/build包的文档有关于命名约定的描述。简单来说,如果文件名包含_$GOOS.go后缀,那么这个源码文件只会在对应的平台被编译。其他平台会忽略这个文件。另一种约定是_$GOARCH.go。这两种后缀可以组合起来,但要保证顺序,正确的格式是_$GOOS_$GOARCH.go,错误的格式是_$GOARCH_$GOOS.go

以下是文件名后缀的一些例子:

1
2
mypkg_freebsd_arm.go // 只在 freebsd/arm 系统编译
mypkg_plan9.go // 只在 plan9 编译

源码文件光有后缀是不够的,比如如下文件名:

1
2
_linux.go
_freebsd_386.go

即使是在linux或freebsd系统,这两个文件也会被忽略,原因是go/build包会忽略所有文件名以._开始的文件。

使用构建标签还是文件名后缀

构建标签和文件名后缀在功能上是重叠的。比如,一个名为mypkg_linux.go的文件,再包含构建标签// +build linux会显得多余。

通常来说,当只有一个特定平台或体系需要指定时,我们选择文件名后缀的方式。比如:

1
2
mypkg_linux.go         // 只在 linux 系统编译
mypkg_windows_amd64.go // 只在 windows amd 64位 平台编译

相反,如果你的文件需要指定给多个平台或体系使用,或者你需要排除某个特定平台时,我们选择构建标签的方式。比如:

1
2
3
4
5
6
7
// 在所有类unix平台编译
% grep '+build' $HOME/go/src/pkg/os/exec/lp_unix.go
// +build darwin dragonfly freebsd linux netbsd openbsd

// 在非Windows平台编译
% grep '+build' $HOME/go/src/pkg/os/types_notwin.go
// +build !windows

总结

本篇文章只关注了Go源码文件,事实上,构建标签和文件名后缀可作用于任何go tool可以编译的源码文件,包括.c.s文件。Go标准库中,尤其是runtime,syscall,os,net包中包含了大量这种例子。

测试文件也支持构建标签和文件名后缀,它们的表现和Go源码文件表现一致。从而允许我们为特定平台指定特定测试用例。

最后,本篇文章讨论的是go tool,事实上,条件编译特性并不局限于这个工具。你可以通过go/build包构建你自己的工具来分析文件名后缀以及构建标签的语法。