时隔 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 又以为是普通注释了。

你是否有在工作中遇到构建的版本、环境约束等场景呢,欢迎大家在评论区留言交流



gogo1.17

311 Words

2021-12-31 12:54 +0800