构建约束
在go中进行编译时,可能会带一些指示条件(如:不同平台、架构)让编译器选择满足条件的代码参与编译,将不满足条件的代码舍弃。这就是条件编译,也可称为构建(编译)约束。目前,支持的构建约束有2种使用方式:
1.文件后缀
2.编译标签(build tag)
两者区别:
文件后缀方式多用于交叉编译 (跨平台)。编译标签方式多用于条件编译 (也可用于交叉编译)。构建约束官方文档
https://pkg.go.dev/cmd/go#hdr-Build_constraints
文件后缀的使用方式
编译器根据文件后缀来选择具体文件来参与编译操作,格式如下:$filenamePrefix_$GOOS.go
$filenamePrefix_$GOARCH.go
$filenamePrefix_$GOOS_$GOARCH.go$filenamePrefix: 源码文件名称前缀(一般为包名称)。
$GOOS: 表示操作系统,从环境变量中获取。
$GOARCH: 表示系统架构,从环境变量中获取。
例如,Go源码中os包的Linux、windows实现
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
使用编译标识
使用编译标识指示编译器选择对应的文件进行编译(也称为: 交叉编译),可以得到非当前平台二进制文件。// 非linux平台编译出linux平台运行的二进制文件
// $filenamePrefix_linux_arm64.go 文件参与编译过程
GOOS=linux GOARCH=arm64 go build
// 非Windows平台编译出Windows平台运行的二进制文件
// $filenamePrefix_windows_arm64.go 文件参与编译过程
GOOS=windows GOARCH=arm64 go build
不使用编译标识
go build不使用编译标识,编译器会根据当前环境编译出当前平台二进制文件。编译标签(build tag)的使用方式
编译标签写法
目前,Go的构建约束支持两种写法:①.// +build <tags>
②.//go:build <tags>
两种编译标签相同点
1.在源码文件顶部添加 (在所有代码之前),来决定文件是否参与编译2.与其他注释之间需要存在一个空行
两种编译标签区别
1.起始位置是否包含空格// +build 与双斜线之间包含空格
//go:build 与双斜线之间不存在空格
2.Go不同的版本支持
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 构建约束。
//go:build 在一个文件中只能存在一行,超过一行则会报错
例如,Go源码 src/math/big/arith_mipsx.s中
//go:build !math_big_pure_go && (mips || mipsle)
// +build !math_big_pure_go
// +build mips mipsle在该文件中,// +build 有两行,//go:build 仅有一行。
4.多个tag之间的连接符
// +build 多个tag之间,可用的连接符
空格表示:AND
逗号表示:OR
!表示:NOT
换行表示:AND//go:build 多个tag之间,可用的连接符
&& 表示: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值
示例
// +build linux,386 darwin,!cgo
表示 (linux && 386) || (darwin && !cgo)
// +build linux darwin
// +build amd64
表示 (linux || darwin) && amd64
// +build ignore
表示 该文件不参与编译过程
自定义tag的使用方式
新建 buildtag 项目,包含文件如下:➜ tree
.
├── demo_not_tag.go
├── demo_tag.go
├── go.mod
└── main.gomain.go 文件
package main
import "fmt"
func main() {
fmt.Println(demo(1, 2))
}demo_tag.go 文件
//go:build use
package main
func demo(a, b int) int {
return a + b + 1
}demo_not_tag.go 文件
//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 build 与 go build -tags use 执行,结果如下所示:
➜ 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.gofunc (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
// 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
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
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.