一种golang编译约束与版本号冲突的缺陷
前言
首先介绍一下golang的版本系统:
Golang语法与工具链的特性以版本号的方式演进。为了避免出现C++、python之类的兼容问题。Golang有严格的版本号限制系统。
Golang 编译约束,也被称为 build tags 或 build constraints,是用来确定当运行 go build
命令时应该包含或排除哪些文件的指令。这些编译约束可以根据操作系统、架构、Go 版本以及自定义的标签来决定。
在 Go 1.17 之前,编译约束通常使用老式的构建约束(如 // +build
注释)来实现。从 Go 1.17 开始,引入了新的构建约束语法 //go:build
,它提供了更为强大和易于理解的逻辑表达能力。尽管 //go:build
是首选的方式,但出于兼容性考虑,Go 仍然支持老式的 // +build
表达式。
以下是与编译约束相关的三者间的关系:
编译约束语法(
// +build
和//go:build
)://go:build
更现代的构建约束,支持复杂的逻辑表达式。// +build
老式的构建约束,使用空格分隔的列表来表示或逻辑,逗号分隔的列表来表示与逻辑。
go.mod 中的版本号:
在
go.mod
文件中指定的 Go 版本号表示模块兼容的最低 Go 语言版本。例如,如果go.mod
指定go 1.18
,那么模块就预计要在 Go 1.18 或更高版本上编译和运行。这个版本号对语言特性的可用性有影响,比如如果你使用了 Go 1.18 引入的泛型特性,那么你的代码就不会在更早的版本上编译通过。当前 Golang 的版本号:
当前你所使用的 Go 工具链的版本,例如 Go 1.18。当你编译项目时,工具链会检查源代码中的编译约束,并决定是否编译某个文件。如果你的工具链版本低于源代码中指定的编译约束(如
//go:build go1.18
),那么这部分代码将不会被包含在编译过程中。
当你看到类似下面的编译约束:
//go:build go1.18
// +build go1.18
这意味着只有当你使用 Go 1.18 或更高版本的编译器时,这段代码才会被包含在编译过程中。如果你试图使用一个低于 1.18 版本的 Go 编译器来编译这段代码,编译器将会忽略这些文件。这就是编译约束与 Go 版本之间的直接关系。而 go.mod
文件中的 Go 版本声明确保了你的模块使用了正确的依赖和编译行为,它与具体的编译约束是分开的,但同时影响着模块的构建过程。
缺陷
原理
在库zap中,为了兼容1.18为分界点的Golang特性(有无泛型、模板)。有两组array实现。
这是一种非常主流的方法。同时维护两套API,以使用户无需重构就能使用心得特性或者修复bug。
如果Golang版本>=1.18
就会使用array_118.go
的特性。但是如果用户当前Golang版本>=1.18
,go.mod
版本中Golang版本<1.18
。就会出现编译问题。这是因为Golang向下兼容,会根据go.mod
中的配置关闭高版本特性。但是构建约束却读取的本地工具链版本。就会出现报错。
# go.uber.org/zap
embedding non-interface type *T requires go1.18 or later (-lang was set to go1.16; check go.mod)
vendor\go.uber.org\zap\array_go118.go:122:13: cannot use &os[i] (value of type *T) as P value in variable declaration
有以下几种解决方法:
降级Golang版本(推荐)
可以使用 g
等工具,切换Golang版本,或者在IDE中指定对应的版本。在这些方法中,都需要安装老版本的Golang。
需要注意的是,如果指定旧版本的Golang,这个版本如果不在环境变量中,在命令行使用工具链,需要指定go.exe
的路径。或者使用IDE,同步与安装依赖。
在编译前自动化切换版本(推荐)
如果使用g。可以在IDE的项目设置中设置编译脚本,或者在CI的脚本中自动切换Golang版本。
提升go.mod版本号
如果将go.mod中得版本号提升也可以解决这个问题。
如果这是协作的项目,建议不要上传这个改动或者联系上游管理员。
直接编辑本地的副本(推荐)
可以在本地缓存中直接修改zap的源码。
如图将约束改为 1.99
后,编译就能正常通过。由于缓存是个人的、纯本地的,所以不会有任何版本问题。或者添加ignore
也行
如果项目使用vendor
或者 go.work
、replace
那需要注意可能会被别人覆盖本地的改动。例如协作者引入了一个第三方库,或者改动了replace的本地库后执行了 vendor
,那会导致改动被覆盖,需要重新操作。如果想要一劳永逸参考下文。
或者引入不存在的一些构建标记也是可行的
本地化第三方库
可以fork一份zap库,将1.18改为1.99。然后replace或者go.work指向这个新的本地库。
不是很推荐这个方法,会导致管理的混乱。
降级zap库
可以将zap降级为1.18版本前的版本。修改go.mod
:
require go.uber.org/zap <appropriate version>
这个方法可能会引入未知的风险,不推荐。
后续
我给golang提了个issue问了下
总结
研究了一小时,没找到完美的解决方案,吐槽下……。当然我的这个情况本身也是挺奇葩的就是了(有python2、python3那味了)。