文章

一种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 表达式。

以下是与编译约束相关的三者间的关系:

  1. 编译约束语法(// +build//go:build):

    • //go:build 更现代的构建约束,支持复杂的逻辑表达式。

    • // +build 老式的构建约束,使用空格分隔的列表来表示或逻辑,逗号分隔的列表来表示与逻辑。

  2. go.mod 中的版本号

    go.mod 文件中指定的 Go 版本号表示模块兼容的最低 Go 语言版本。例如,如果 go.mod 指定 go 1.18,那么模块就预计要在 Go 1.18 或更高版本上编译和运行。这个版本号对语言特性的可用性有影响,比如如果你使用了 Go 1.18 引入的泛型特性,那么你的代码就不会在更早的版本上编译通过。

  3. 当前 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.18go.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.workreplace 那需要注意可能会被别人覆盖本地的改动。例如协作者引入了一个第三方库,或者改动了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那味了)。

License:  CC BY 4.0