时隔 3 年,Go1.17 增强构建约束!
大家好,我是煎鱼。
Go1.17rc1 在前几天终于正式发布了:
看到 Go1.17 增加了一个新特性,是面向 Go 构建时的构建约束的增强。认真一看,是一个时隔 3 年的提案了,原本还在 Go2 和 Go1 之间左右摇摆,这下在 6 月底 Russ Cox 就输出了新草案:《Bug-resistant build constraints — Draft Design》。紧接着直接计划在 Go1.17 发布了。
一气呵成,真实版高效能了。
如下图:
之前小咸鱼有遇到好几个朋友,在报错时压根不知道 Go 有这个约束语法,以为只是个单纯的注释,直接不明所以然,感觉科普之路任重道远。
今天这篇文章煎鱼就来讲讲构建约束这事。
注:下个月 Go1.17 就会正式发布,距离 Go1.18 泛型出山只差一点点距离了,值得期待!
构建约束的背景
简单来讲,在真实环境中,可能需要为不同的编译环境编写不同的 Go 代码,所以需要做构建约束。
划重点,Go 语言对这一问题的解决方案是在文件层面进行有条件的编译:每个文件要么在编译中,要么不在。
也就是,假设不符合构建约束的场景。那么会直接不编译这个文件,因为他不在编译范围内。那在程序想运行时就会报错,表示找不到文件。因此有许多的同学看着报错信息,经常找不着北…
现有的构建约束
既然是叫 “增强”。说明现有就有构建约束。最早的构建约束是在 2011 年 9 月引入的构建约束。
我们平时常见的构建约束(build constraint),也叫做构建标记(build tag),构建约束必须出现在 package
之前。
平时会在 Go 工程的文件中的最开始会看到如下行注解:
// +build
为了将构建约束与包文档区分开来,构建约束后必须跟一个空行。
// +build linux,386 darwin,!cgo
又或是:
// +build linux darwin
// +build amd64
还可以根据 Go 版本来约束:
// +build go1.9
其主要支持如下几种:
- 指定编译的操作系统,例如:windows、linux 等,对应
runtime.GOOS
的值。 - 指定编译的计算机架构,例如:amd64、386,对应
runtime.GOARCH
的值。 - 指定使用的编译器,例如:gccgo、gc。
- 指定 Go 版本,例如:go1.9、go1.10 等。
- 指定自定义的标签,例如:编译时通过指定
-tags
传入的值。 - …
有什么问题
既要用动他,本着 Go team 的 less is more 原则。想必是现有的构建约束,存在着什么问题,才需要调整他。
对语法困惑
从 issues 的反馈来看,是太复杂,如下:
// +build linux,386 darwin,!cgo
他表达的构建约束是:(linux AND 386) OR (darwin AND (NOT cgo)) 。
感觉可以像三元运算符一样玩出花,可参见《Go 凭什么不支持三元运算符?》,这更夸张,没常见的逻辑符。
也可以更复杂一些:
// +build 386 !gccgo,amd64 !gccgo,amd64p32 !gccgo
会导致会混淆用户的认知,如果能够这样写更好:
// +build 386 amd64 amd64p32
// +build !gccgo
对布局困惑
现在的 // +build
有硬性的使用规则:
- 必须出现在文件顶部附近,前面只能有空行和其他行注释。这些规则意味着在 Go 文件中,构建约束必须出现在 package 子句之前。
- 为了将构建约束与包文档区分开来,一系列构建约束后必须跟一个空行。
像是以下失败案例:
package main
// +build linux
又或是:
/*
Copyright ...
*/
// +build linux
package main
整体来看,官方在 2020 年 3 月对 // +build
注解的使用情况进行了分析,得出以下几种常见情况:
- 忽略了
/* */
注释后的构建约束,通常是版权声明。 - 忽略了文档注释中的构建约束。
- 忽略了包声明后的构建约束。
这些都是实际项目中出现的,也就是这个构建约束的布局约束并不好,造成了很多意外和反馈。
像是版权声明的统一写入,基本都是脚本统一打上去的。会造成大量的隐藏版本 BUG。
增强后的构建约束
增强,就是优化,主要的目标之一是解决语法、布局困惑。
设计的核心思想:用新的 //go:build
取代目前用于构建标签选择的 //+build
,并且使用更广为熟悉的布尔表达式。
设计的关键:平滑过渡,避免破坏 Go 代码。
以前老的注解:
// +build linux
// +build 386
新的注解:
//go:build linux && 386
新的语法主体为 Go spec 的 EBNF 标记:
BuildLine = "//go:build" Expr
Expr = OrExpr
OrExpr = AndExpr { "||" AndExpr }
AndExpr = UnaryExpr { "&&" UnaryExpr }
UnaryExpr = "!" UnaryExpr | "(" Expr ")" | tag
tag = tag_letter { tag_letter }
tag_letter = unicode_letter | unicode_digit | "_" | "."
也就是说,构建标记的语法与其当前形式保持不变,但构建标记的组合现在使用 Go 的 ||、&& 和 ! 运算符和括号完成。
另外一个文件只能有一行构建语句,也就是一个文件有多行 //go:build
是错误的,如此设计的目的是为了消除关于多行是隐式 AND 还是 OR 在一起的混淆。
过渡阶段
在过渡阶段,也就是 Go1.17 起。官方的 gofmt 工具会自动根据旧语法转换新版的语法,以保证兼容性。
例如:
// +build !windows,!plan9
会转变为:
//go:build !windows && !plan9
// +build !windows,!plan9
后面是计划把 //+build
给完全下线的。
常规 Go 工程基本用不到,因此就不进一步展开描述了。
对过渡阶段感兴趣的可以看看 《Bug-resistant build constraints — Draft Design》的 Transition 部分,比较长,正常来讲是不需要我们关注的。
总结
Go 1.17 构建约束的增强,一下子让整个语法明确了起来。统一为 //go:build
,至少不会有人看到 //+build
又以为是普通注释了。
你是否有在工作中遇到构建的版本、环境约束等场景呢,欢迎大家在评论区留言交流!