Go 构建约束的使用方式

在go中进行编译时,可能会带一些指示条件(如:不同平台、架构等)让编译器选择满足条件的代码参与编译,将不满足条件的代码舍弃。这就是条件编译,也可称为构建(编译)约束

目前,支持的构建约束有2种使用方式:
1.文件后缀
2.编译标签(build tag)

两者区别:

  • 文件后缀方式多用于交叉编译 (跨平台)。
  • 编译标签方式多用于条件编译 (也可用于交叉编译)。

构建约束官方文档

https://pkg.go.dev/cmd/go#hdr-Build_constraints

文件后缀的使用方式

编译器根据文件后缀来选择具体文件来参与编译操作,格式如下:

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

$filenamePrefix: 源码文件名称前缀(一般为包名称)。
$GOOS: 表示操作系统,从环境变量中获取。
$GOARCH: 表示系统架构,从环境变量中获取。

例如,Go源码中os包的Linux、windows实现

1
2
3
4
5
6
src/runtime/os_linux.go
src/runtime/os_linux_arm.go
src/runtime/os_linux_arm64.go
src/runtime/os_windows.go
src/runtime/os_windows_arm.go
src/runtime/os_windows_arm64.go

使用编译标识编译

使用编译标识指示编译器选择对应的文件进行编译(也称为: 交叉编译),可以得到非当前平台二进制文件。

1
2
3
4
5
6
7
// 非linux平台编译出linux平台运行的二进制文件
// $filenamePrefix_linux_arm64.go 文件参与编译过程
GOOS=linux GOARCH=arm64 go build

// 非Windows平台编译出Windows平台运行的二进制文件
// $filenamePrefix_windows_arm64.go 文件参与编译过程
GOOS=windows GOARCH=arm64 go build

不使用编译标识编译

1
go build

不使用编译标识,编译器会根据当前环境编译出当前平台二进制文件。

编译标签(build tag)的使用方式

编译标签写法

目前,Go的构建约束支持两种写法:
①.// +build <tags>
②.//go:build <tags>

两种编译标签相同点

1.在源码文件顶部添加 (在所有代码之前),来决定文件是否参与编译
2.与其他注释之间需要存在一个空行

两种编译标签区别

1.起始位置是否包含空格
// +build 与双斜线之间包含空格
//go:build 与双斜线之间不存在空格

2.Go不同的版本支持

1
2
Go versions 1.16 and earlier used a different syntax for build constraints, with a "// +build" prefix. 
The gofmt command will add an equivalent "//go:build" constraint when encountering the older syntax.

在Go的1.16以及之前的版本使用 // +build 前缀来标识构建约束。
gofmt命令在遇到 // +build 前缀来标识构建约束时会添加一下等效的 //go:build 构建约束。

3.同一文件中编译标签的行数

// +build 在一个文件中可以存在多行
//go:build 在一个文件中只能存在一行,超过一行则会报错

例如,Go源码 src/math/big/arith_mipsx.s

1
2
3
//go:build !math_big_pure_go && (mips || mipsle)
// +build !math_big_pure_go
// +build mips mipsle

在该文件中,// +build 有两行,//go:build 仅有一行。

4.多个tag之间的连接符

// +build 多个tag之间,可用的连接符

1
2
3
4
空格表示:AND
逗号表示:OR
!表示:NOT
换行表示:AND

//go:build 多个tag之间,可用的连接符

1
2
3
4
&& 表示:AND
|| 表示:OR
! 表示:NOT
() 表示:分组

从这里可以看出 //go:build 多个tag之间的连接符更接近于代码规范,也更加容易理解(这也是替代// +build的一个原因)。

编译标签中多个tag的组合方式

tag 可指定为以下内容:

  • 操作系统,环境变量中GOOS的值

    如:linux、darwin、windows等等

  • 操作系统的架构,环境变量中GOARCH的值

    如:arch64、x86、i386等等

  • 使用的编译器

    如:gc或者gccgo,是否开启CGO,cgo

  • golang版本号

    如:Go Version 1.1为go1.1, Go Version 1.12版本为go1.12,以此类推

  • 其它自定义标签

    通过 go build -tags 自定义tag名称 指定tag值

示例

1
2
3
4
5
6
7
8
9
10
11
// +build linux,386 darwin,!cgo
表示 (linux && 386) || (darwin && !cgo)


// +build linux darwin
// +build amd64
表示 (linux || darwin) && amd64


// +build ignore
表示 该文件不参与编译过程

自定义tag的使用方式

新建 buildtag 项目,包含文件如下:

1
2
3
4
5
6
➜  tree                           
.
├── demo_not_tag.go
├── demo_tag.go
├── go.mod
└── main.go

main.go 文件

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println(demo(1, 2))
}

demo_tag.go 文件

1
2
3
4
5
6
7
//go:build use

package main

func demo(a, b int) int {
return a + b + 1
}

demo_not_tag.go 文件

1
2
3
4
5
6
7
//go:build !use

package main

func demo(a, b int) int {
return a + b
}

从上面代码可以看到 demo_tag.go 文件中 //go:build use 与 demo_not_tag.go 文件中 //go:build !use

分别使用 go buildgo build -tags use 执行,结果如下所示:

1
2
3
4
5
6
➜  go build
➜ ./buildtag
3
➜ go build -tags use
➜ ./buildtag
4

可以看出:

  • 使用 go build 调用的demo方法为 demo_not_tag.go 文件中demo方法
  • 使用 go build -tags use 调用的demo方法为 demo_tag.go 文件中demo方法

如果有多个tag可以使用空格分隔
例如:go build -tags "use use1 use2"

总结一句话就是:编译器根据 tag标识 有选择性的加载对应文件进行编译

Go源码中关于构建约束的部分追溯

src/cmd/asm/internal/lex/tokenizer.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (t *Tokenizer) Next() ScanToken {
s := t.s

for {
t.tok = ScanToken(s.Scan())
if t.tok != scanner.Comment {
break
}
text := s.TokenText()
t.line += strings.Count(text, "\n")
// TODO: Use constraint.IsGoBuild once it exists.
if strings.HasPrefix(text, "//go:build") {
t.tok = BuildComment
break
}
}

......

return t.tok
}

在词法分析时, 存在 "//go:build" 开头的文本,则标识为 token 为 BuildComment

src/cmd/asm/internal/lex/lex.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A ScanToken represents an input item. It is a simple wrapping of rune, as
// returned by text/scanner.Scanner, plus a couple of extra values.
type ScanToken rune

const (
// Asm defines some two-character lexemes. We make up
// a rune/ScanToken value for them - ugly but simple.
LSH ScanToken = -1000 - iota // << Left shift.
RSH // >> Logical right shift.
ARR // -> Used on ARM for shift type 3, arithmetic right shift.
ROT // @> Used on ARM for shift type 4, rotate right.
Include // included file started here
BuildComment // //go:build or +build comment
macroName // name of macro that should not be expanded
)

BuildComment 代表 //go:build or +build 注释

src/cmd/fix/buildtag.go

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
41
42
43
44
45
46
47
package main

import (
"go/ast"
"strings"
)

func init() {
register(buildtagFix)
}

const buildtagGoVersionCutoff = 1_18

var buildtagFix = fix{
name: "buildtag",
date: "2021-08-25",
f: buildtag,
desc: `Remove +build comments from modules using Go 1.18 or later`,
}

func buildtag(f *ast.File) bool {
if goVersion < buildtagGoVersionCutoff {
return false
}

// File is already gofmt-ed, so we know that if there are +build lines,
// they are in a comment group that starts with a //go:build line followed
// by a blank line. While we cannot delete comments from an AST and
// expect consistent output in general, this specific case - deleting only
// some lines from a comment block - does format correctly.
fixed := false
for _, g := range f.Comments {
sawGoBuild := false
for i, c := range g.List {
if strings.HasPrefix(c.Text, "//go:build ") {
sawGoBuild = true
}
if sawGoBuild && strings.HasPrefix(c.Text, "// +build ") {
g.List = g.List[:i]
fixed = true
break
}
}
}

return fixed
}

Remove +build comments from modules using Go 1.18 or later 明确说明在 1.18或者更新的版本中会移除 +build

src/cmd/fix/buildtag_test.go

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
package main

func init() {
addTestCases(buildtagTests, buildtag)
}

var buildtagTests = []testCase{
{
Name: "buildtag.oldGo",
Version: 1_10,
In: `//go:build yes
// +build yes

package main
`,
},
{
Name: "buildtag.new",
Version: 1_99,
In: `//go:build yes
// +build yes

package main
`,
Out: `//go:build yes

package main
`,
},
}

buildtagTests中 In、Out可以看出不同版本对于 // +build 的处理方式。

fix的作用

Fix finds Go programs that use old APIs and rewrites them to use newer ones.
After you update to a new Go release, fix helps make the necessary changes to your programs.