标签 go.mod 下的文章

Go 1.24中值得关注的几个变化

本文永久链接 – https://tonybai.com/2025/02/16/some-changes-in-go-1-24

北京时间2025年2月12日,恰逢中国传统元宵佳节,远在美国的Go团队正式发布了Go 1.24的第一个版本Go 1.24.0。这也是Go团队在更换Tech Leader为Austin Clements后发布的首个大版本。

按照惯例,每次Go大版本发布时,我都会撰写一篇“Go 1.x中值得关注的几个变化”的文章。自2014年的Go 1.4版本起,这一系列文章已经持续了11年。

不过,随着从Go 1.17版本开始引入的“Go 1.x新特性前瞻”系列以及针对特定技术特性的专题文章,“Go 1.x中值得关注的几个变化”系列文章的形式也在不断演变。原先的“Go 1.x中值得关注的几个变化”可以理解为被目前的“Go 1.x新特性前瞻” + “特定技术特性文章” + “Go 1.x中值得关注的几个变化”所替代。

不过,随着从Go 1.17版本开始引入的“Go 1.x新特性前瞻”系列以及针对特定技术特性的专题文章,“Go 1.x中值得关注的几个变化”系列的形式也在不断演变。原先的“Go 1.x中值得关注的几个变化”已逐渐被目前的“Go 1.x新特性前瞻” + “特定技术特性文章” + “Go 1.x中值得关注的几个变化(新版)”所替代。希望各位读者能够理解这种变化,“Go 1.x中值得关注的几个变化”系列依然会延续,但文章中将不再进行细致的分析,因为这些内容已经在之前的前瞻和专题文章中讨论过了。

好了,言归正传,我们来说说Go 1.24!

1. 语言变化

正如Go一贯所做的,新版Go 1.24.0继续遵循Go1的兼容性规范。使用Go 1.24.0,你可以顺利编译和运行你用Go 1.11编写的代码。相信许多Gopher正是因为这一点而喜欢上Go,就像下面这位Gopher在Go 1.24发布后所表现出的惊喜一样:

不过,正如Go一贯所做的那样,在语法特性方面,Go显得十分“吝啬”。在Go 1.18大方地引入了泛型之后,Go团队又恢复了这种“吝啬”的风格。在Go 1.24的发布说明中,那短短的一行字充分展现了这一特点:

我们看到,Go 1.24仅仅是将Go 1.23版本中的实验特性“带有类型参数的类型别名”转正了,成为了默认特性。当然你仍然可以GOEXPERIMENT=noaliastypeparams显式关闭它。关于这个特性的具体内容,我们多次说过了,大家可以到《Go 1.24新特性前瞻:语法、编译器与运行时》温习一下它的具体内容。

不过这种“吝啬”也是很多Gopher所期望的,当年Go语言之父Rob Pike在“Simplicity is Complicated”演讲中提到的如下权威观点,影响了诸多Gopher,当然也包括我:

因此,在正在如火如荼的“spec: reduce error handling boilerplate using ?”的讨论中,就当前的情况来看,我也倾向于保持现状

2. 编译器与运行时

在2024年中旬,Fasthttp的作者、VictoriaMetrics的联合创始人Aliaksandr Valialkin曾因Go加入自定义函数迭代的特性而发文抱怨“Go正在朝着错误的方向演进”。不过他也提到,如果Go团队专注于提升Go的性能,而不是在与社区争论一些“华而不实”的语法糖,可能会赢得更多开发者的青睐:

尽管Go 1.24尚未添加对SIMD的官方支持,但引入的优化显然不会让Aliaksandr Valialkin失望。首当其冲的就是对map底层实现的优化——使用更为高效的Swiss Table。关于Swiss Table及Go 1.24重写map的思路,可以参考我的《Go map使用Swiss Table重新实现,性能最高提升近50%》一文。根据文中的实测结果,新版基于Swiss Table的map在多数测试项中表现出显著的性能提升,有些甚至接近50%!

当然,基于Swiss Table的map实现仍在不断完善,其实现者Michael Pratt将持续进行打磨和优化:

参与Go Swiss Table重写方案讨论,并提供参考实现之一的CockroachDB CTO Peter Mattis,也在X.com上分享了新map设计和实现的诞生过程与优势,大家可以阅读以加深理解。

此外,Go 1.24还优化了runtime内部的锁实现,新实现在高竞争情况下取得了显著的可扩展性提升,而不是像Go 1.24之前的实现那样随线程数增加而急剧下降。基准测试表明,在GOMAXPROCS=20时,性能提升达3倍。

更多编译器和运行时的变化,可以参考《Go 1.24新特性前瞻:语法、编译器与运行时》。

Go 1.24版本在编译器和运行时方面的优化投入和勇于改变,正是Go社区所期望的。相信后续版本在这方面的持续投入不会让Aliaksandr Valialkin失望。

3. 工具链

Go团队在Go工具链上的投入和结果一直被Go社区认可和赞扬!《Go 1.24新特性前瞻:工具链和标准库》一文中有对Go 1.24工具链变化的详细介绍,但在这里我还是要再次提及其中的三个变化。

go.mod增加tool指示符,支持对tool的依赖管理

借用《Go工具链版本已不由你定:go和toolchain指令详解》中的那幅图:

Go的目标显然是要实现对Go应用所依赖“全要素”进行版本管理”,涵盖Go版本、工具链版本、第三方包版本以及依赖工具版本的管理。而Go 1.24在go.mod中增加tool指示符就是要实现对依赖工具的版本进行管理。增加tool指示符后,你可以像管理第三方包版本那样,使用go get -tool对依赖的tool的版本进行管理,go install tool对tool进行安装,并支持一个tool同时存在多个版本在本地,这是由于通过go.mod管理的依赖的tool会被像module那样缓存在本地构建缓存中(go build cache)。

btw,再说说go 1.24对toolchain依赖管理和选择的改善。即便看了《Go工具链版本已不由你定:go和toolchain指令详解》一文,很多Gopher还是可能因为gotoolchain决策的复杂性和参与要素的众多而感到困惑,Go 1.24增加了GODEBUG=toolchaintrace=1可以输出做出决策的过程日志,告诉你Go为何会选择某个特定的toolchain版本。

go vet的增强

在Go 1.24中,go vet的功能有了较大变化,新增或增强了如下一些分析器(analyzer):

  • 新增测试分析器

可以检测test、fuzz test、基准测试和example test中的常见错误,避免因命名、签名错误或引用不存在的标识符而导致测试无法运行。

  • printf分析器增强

新增对fmt.Printf(s)的检查,如果这类调用中格式字符串并非常量且没有传入其他参数,则提醒用户使用fmt.Print。

  • buildtag分析器增强

新增对无效Go主版本构建约束的检测,避免错误引用次版本号。例如,如果你使用//go:build go1.23.1,该分析器会提醒你应该使用//go:build go1.23。

  • copylock分析器增强

增强对经典三段式for循环中包含sync.Locker的变量复制的不安全操作的诊断,防止锁的复制带来的潜在问题。这也是Go 1.23修正loopvar语义后避免Go用户误用的一个防卫手段。

新增GOCACHEPROG

另一个大家可能忽视的值得关注的改变是新增了GOCACHEPROG环境变量。

Go语言的cmd/go工具已经具备了强大的缓存支持,但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高,尤其是在CI(持续集成)环境中,用户通常需要将GOCACHE目录打包和解压缩,这往往比CI操作本身还要慢。此外,用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率,但这些功能并不适合直接集成到cmd/go工具中。

为了解决上述问题,Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG,类似于现有的GOCACHE变量。通过设置GOCACHEPROG,用户可以指定一个外部程序,该程序将作为子进程运行,并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口与外部缓存程序交互,外部程序可以根据需要实现任意的缓存机制和策略。其大致结构如下:

显然一旦可以在云上存储build cache,也能缓解一下Go用户抱怨本地缓存过大的问题。当然从实际情况来看(我的本地环境),go build cache还不是最大的:

$go env|grep CACHE
GOCACHE='/Users/tonybai/Library/Caches/go-build'
GOCACHEPROG=''
GOMODCACHE='/Users/tonybai/Go/pkg/mod'
$cd /Users/tonybai/Library/Caches/go-build
$du -sh
155M    .

$cd /Users/tonybai/Go/pkg/mod
$du -sh
7.0G

我们看到在我本地的环境中,go build cache和go module cache的size相比,简直是不值得一提,所以说如果后续要有个GOMODCACHEPROG就更好了,我也十分希望能将go module cache搬移到云端(比如S3)中,甚至可以让组织内的Gopher共享这些go module cache(当然要区分不同arch和os)。

更多关于工具链的变化,可以参考《Go 1.24新特性前瞻:工具链和标准库》。

4. 标准库

Go标准库向来是变化的大户,这里我显然不会列出所有变化,甚至一些值得关注的变化,比如:json包增加对omitzero选项的支持新增weak包和weak指针等,也都在新特性前瞻或技术专题性文章中有过详细说明。

这里要说的是Go对fips 140-3合规性的支持,因为这个最终版本与当初新特性前瞻时有所变化。

基于最新的Go fips 140-3文档,我们可以得到关于fips 140-3使用方法的说明,这里简要梳理如下:

  • Go 1.24及更高版本开始,Go二进制文件可以原生运行在FIPS 140-3合规模式下,不必依赖注入boringssl等第三方C++包。
  • Go新增了的一个特殊的Go加密模块 (Go Cryptographic Module),其下有一组新增的标准库包(位于crypto/internal/fips140/…下),实现了 FIPS 140-3批准的算法。这个cryptographic module的版本当前为v1.0.0,目前正在接受CMVP认证实验室的测试。

Go引入了GOFIPS140环境变量,用于go build、go install和go test命令,以选择要链接到可执行程序中的Go加密模块版本。该环境变量有三类可选值:

  • off (默认): 使用标准库中的crypto/internal/fips140/…包。
  • latest: 类似off,但默认启用FIPS 140-3模式。
  • v1.0.0: 使用Go加密模块 v1.0.0 版本(在Go 1.24中首次发布,并在2025年初冻结),默认启用FIPS 140-3模式。

在运行时,可以通过GODEBUG=fips140=xxx来控制上述编译到Go中的Go cryptographic module是否运行在FIPS 140-3模式下,默认是off。

当使用GODEBUG=fips140=on时,Go运行时将会启用Go cryptographic module的FIPS 140-3模式。启用后,Go加密模块会执行以下操作:

  • 完整性自检: 在init阶段,会验证模块对象文件的校验和,确保代码未被篡改。
  • 已知答案自检: 根据FIPS 140-3指南,在init阶段或首次使用时,对算法进行已知答案测试。
  • 密钥一致性测试: 对生成的密钥进行配对一致性测试 (这可能导致某些密钥类型生成速度减慢,特别是临时密钥)。
  • crypto/rand.Reader改进: 使用NIST SP 800-90A DRBG,并从平台CSPRNG获取随机字节混合到输出中。
  • crypto/tls限制: 仅协商符合NIST SP 800-52r2 的协议版本、密码套件、签名算法和密钥交换机制。
  • crypto/rsa.SignPSS限制: 使用PSSSaltLengthAuto时,盐的长度会被限制为哈希的长度。

当使用GODEBUG=fips140=only时,不符合FIPS140-3的加密算法会返回错误或者panic。但是此模式仅为尽力而为,不保证符合所有的FIPS 140-3要求。

不过大家要知道的是:在Go 1.24版本中,GODEBUG=fips140=on和only在OpenBSD、Wasm、AIX和32位Windows平台上暂不受支持。

另外要想要检测FIPS 140-3模式是否已经激活,可以调用crypto/fips140.Enabled函数。

之前,一些场合用户使用BoringCrypto模块来实现某些FIPS 140-3算法的机制仍然可用,但已不被官方支持,并计划在未来版本中移除。另外要知道Go+BoringCrypto与原生FIPS 140-3模式并不兼容。这也是Microsoft Go依旧宣称将保留自己维护的符合fips140-3的Go版本的原因

5. 其他

最后重点说说WebAssembly port。

Go从Go 1.11版本开始通过js/wasm增加了对编译到Wasm的支持。Go 1.21版本又增加了对WASI的支持(GOOS=wasip1),Go 1.24版本中,Go对Wasm的支持又有了新的特性。在Go 1.24发布没多久,Cherry Mui便在官博发表了名为“Extensible Wasm Applications with Go”的介绍Go 1.24中WebAssembly新特性的文章,文章介绍了Go 1.24对Wasm的支持程度以及一些限制。这里也参考了这篇文章,简单梳理一下Cherry给出的内容要点。

Go 1.24引入了新的编译器指示符go:wasmexport,允许将Go函数导出,以便从Wasm模块外部(通常是从运行Wasm运行时的主机应用程序)调用。该指示符指示编译器将带注释的函数作为Wasm导出提供,在生成的Wasm二进制文件中可用,比如:

//go:wasmexport add
func add(a, b int32) int32 { return a + b }

这样,Wasm模块将具有一个名为add的导出函数,可以从主机调用。

这是如何实现的呢?Cherry告诉我们这是通过构建一种名为WASI Reactor的Wasm模块来实现的。WASI Reactor是一种持续运行的WebAssembly模块,可以多次调用以响应事件或请求。与在主函数完成后终止的“命令”模块不同,reactor实例在初始化后保持活动状态,其导出保持可访问状态。

在Go 1.24中,要构建一个WASI reactor需要使用下面命令:

$GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm

该构建命令指示链接器不生成_start函数(wasm命令模块的入口点),而是生成_initialize函数(执行运行时和包初始化)以及任何导出的函数及其依赖项。_initialize函数必须在任何其他导出函数之前调用。而main函数不会自动调用。

go:wasmexport指示符和reactor构建模式允许通过调用基于Go的Wasm代码来扩展应用程序。这对于采用Wasm作为具有明确定义接口的插件或扩展机制的应用程序特别有价值。通过导出Go函数,应用程序可以利用Go Wasm模块提供功能,而无需重新编译整个应用程序。此外,构建为reactor可确保可以多次调用导出的函数而无需重新初始化,使其适用于长时间运行的应用程序或服务。

次卧,Go 1.24还放宽了对可用于go:wasmimport函数的输入和结果参数类型的限制。例如,可以传递bool、string、指向int32的指针或指向嵌入structs.HostLayout并包含受支持字段类型的结构体的指针,这使得Go Wasm应用程序可以用更自然的方式编写,并消除了不必要的类型转换。

不过,go:wasmexport当前也有局限性,首先,Wasm 是单线程架构,没有并行性。go:wasmexport标识的函数可以生成新的goroutine。但是,如果函数创建了后台goroutine,则当go:wasmexport指示的函数返回时,它将不会继续执行,直到回调到基于Go的Wasm模块。

另外,尽管Go 1.24中放宽了一些类型限制,但对于可与go:wasmimport和go:wasmexport函数一起使用的类型仍然存在限制。比如由于客户端的64位体系结构和主机的32位体系结构之间的不匹配,我们无法传递内存中的指针。例如,go:wasmimport指示的函数不能采用指向包含指针类型字段的结构体的指针。

但不可否认的是go:wasmexport的支持,让Go更稳固了自己成为主流wasm开发语言之一的位置,虽然还有各种不足。近期Docker之父的初创公司Dagger就发博客宣称使用了Go+WebAssembly重写了其Dagger Cloud的前端

6. 小结

Go 1.24的发布,标志着Go语言在保持其核心理念——简洁与兼容性的同时,进入了一个新的发展阶段。这个版本没有在语法上大刀阔斧,而是将重心放在了底层性能优化、工具链完善和新兴技术布局上,展现出Go团队务实且具有前瞻性的发展策略。同时,Go 1.24也可以看成是一个承上启下的版本。它既巩固了Go语言在性能和工具链方面的优势,又为未来的发展方向做出了积极的布局。Go语言正以稳健的步伐,朝着更高效、更安全、更具适应性的方向迈进。我们可以期待,在未来的版本中,Go将继续在云原生计算、WebAssembly、AI应用等领域发挥更大的作用,为开发者带来更多的惊喜。

借此文章插播一条国内Go社区的news!

近期GoCN社区发文“Farewell Go,Hello AI:是时候说再见了”和所有国内Go开发人员分享了“GoCN社区将正式转型升级为ThinkInAI社区”,全面拥抱AI的决定!这也意味国内最大Go技术社区的退出,最大Go技术大会GopherChina的正式落幕!除了AI是热门赛道这一原因之外,文章也给出了Go技术分享遇到瓶颈的说法:

不过就像文中所说的“这是任何技术发展到成熟阶段的必然现象”,在评论中一些Gopher也提到:一个技术不再被更多讨论是成熟的标志

这其实与我在《2024年Go语言盘点:排名历史新高,团队新老传承》一文中表达的Go演进趋势不谋而合!Go真的进入了成熟期了!

GoCn不在了,但go在国内的传播和使用依然会继续。请继续关注诸如Gopher Daily、我的公众号以及国内其他诸如像鸟窝老师的blog以及公众号,了解Go的最新动态以及技术理解。

最后感谢AstaXie(谢孟军)对国内Go社区发展所做出的卓越贡献,我也因有幸多次参与GopherChina以及会上分享而感到无比自豪。


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go工具链版本已不由你定:go和toolchain指令详解

本文永久链接 – https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod

Go语言自诞生以来,就一直将向后兼容性作为其核心理念之一。Go1兼容性承诺确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础,它不仅保障了稳定性,也大大减轻了随着语言演进带来的代码维护负担。然而,兼容性的内涵并不仅限于向后兼容。向前兼容性,即旧版本的工具链能够优雅地处理针对新版本编写的代码,对于打造流畅的开发体验同样至关重要。

Go 1.21版本之前,向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本,但在实际中,它更像是一个指导性建议,而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码,这经常导致令人困惑的错误,更有甚者会出现“静默成功”的情况——代码虽然可以编译,但由于较新版本中的细微改动,其运行时行为可能并不正确。

Go 1.21的发布标志着这一现状的重大转变。该版本引入了健壮且自动化的工具链管理机制,将go指令转变为一项强制性要求,并简化了使用不同Go版本进行开发的工作流程。即将发布的Go 1.24版本在此基础上进一步增强,引入了tool指令,允许开发者指定对外部工具及其特定版本的依赖,从而进一步提升了代码的可重复性和项目的可维护性。

这些改进进一步明确和巩固了go命令作为全方位依赖管理器的角色定位,它不仅管理外部模块,还负责管理Go工具链版本,以及越来越多的外部开发工具(如下图):

不过向前兼容性规则的明确以及toolchain指令的引入也给Go开发者带来一定的理解上的复杂性,并且在使用Go 1.21版本之后,我们可能遇到会遇到一些因Go工具链版本选择而导致的编译问题。

本文将通过一系列典型场景和详细的示例,帮助读者全面理解Go向前兼容性的规则,以及go指令以及toolchain指令对Go工具链选择的细节,从而让大家能更加自信地驾驭Go开发中不断演进的技术环境。

接下来,我们就从对向前兼容性的理解开始!

1. 理解向前兼容性

向前兼容性,在编程语言的语境中,指的是旧版本的编译器或运行时环境能够处理针对该语言的新版本编写的代码。它与向后兼容性相对,后者确保的是新版本的语言能够处理为旧版本编写的代码。向后兼容性对于维护现有代码库至关重要,而向前兼容性则是在使用不断演进的语言和依赖项时获得流畅开发体验的关键所在。

向前兼容性的挑战源于新语言版本通常会引入新的特性、语法变更或对标准库的修改。如果旧的工具链遇到了依赖于这些新元素的代码,它可能无法正确地编译或解释这些代码。理想情况下,工具链应该能够识别出代码需要一个更新的版本,并提供清晰的错误提示,从而阻止编译或执行。

在Go 1.21之前的版本中,向前兼容性并没有得到严格的保证。让我们来看一个例子。我们用Go 1.18泛型语法编写一个泛型函数Print:

// toolchain-directive/demo1/mymodule.go
package mymodule

func Print[T any](s T) {
    println(s)
}

// toolchain-directive/demo1/go.mod
module mymodule

go 1.18

如果你尝试使用Go 1.17版本来构建这个模块,你将会遇到类似以下的错误:

$go version
go version go1.17 darwin/amd64

$go build
# mymodule
./mymodule.go:3:6: missing function body
./mymodule.go:3:11: syntax error: unexpected [, expecting (
note: module requires Go 1.18

这些错误信息具有一定的误导性,它们指向的是语法错误,而不是问题的本质:这段代码使用了Go 1.18版本中才引入的泛型特性。虽然go命令确实打印了一条有用的提示(note: module requires Go 1.18),但对于规模大一些的项目来说,在满屏的编译错误中,这条提示很容易被忽略。

而比上面这个示例更隐蔽的问题是所谓的“静默成功”。

设想这样一个场景:Go标准库中的某个bug在Go 1.19版本中被修复了。你编写了一段代码,并在不知情的情况下依赖于这个bug修复。如果你没有使用任何Go 1.19版本特有的语言特性,并且你的go.mod文件中指定的是go 1.19,那么旧版本的Go 1.18工具链将会毫无怨言地编译你的代码并获得成功。然而,在运行这段代码时,你的程序可能会表现出不正确的行为,因为那个bug在Go 1.18的标准库中依然存在。这就是“静默成功”——编译过程没有任何错误提示,但最终生成的程序却是有缺陷的。

在Go 1.21版本之前,go.mod文件中的go指令更多的是一种指导性意见。它表明了期望使用的Go版本,但旧的工具链并不会严格执行它。这种执行上的疏漏是导致Go开发者面临向前兼容性挑战的主要原因。

Go 1.21版本从根本上改变了go指令的处理方式。它不再是一个可有可无的建议,而是一个强制性的规则。下面我们就来看看Go 1.21及更高版本中是如何确保向前兼容性的。由于多数情况下,我们不会显式在go.mod显式指定toolchain指令,因此,我们先来看看没有显式指定toolchain指令时,go指令对向前兼容性的影响

2. 作为规则的go指令:确保向前兼容性(Go 1.21及更高版本)

Go 1.21对Go version、language version、release version等做了更明确的定义,我们先来看一下,这对后续理解go.mod文件中go指令的作用很有帮助。下图形象的展示了各个version之间的关系:

Go版本(Go Version),也是发布版本(Release Version)使用1.N.P的版本号形式,其中1.N称为语言版本(language version),表示实现该版本Go语言和标准库的Go版本的整体系列。1.N.P是1.N语言版本的一个实现,初始实现是1.N.0,也是1.N的第一次发布!后续的1.N.P成为1.N的补丁发布。

任何两个Go版本(Go version)都可以进行比较,以判断一个是小于、大于还是等于另一个。

如果语言版本不同,则语言版本的比较结果决定Go版本的大小。比如:1.21.9 vs. 1.22,前者的语言版本是1.21,后者语言版本是1.22,因此1.21.9 < 1.22。

如果语言版本相同,从小到大的排序为:语言版本本身、按R排序的候选版本(1.NrcR),然后按P排序的发布版本,例如:

1.21 < 1.21rc1 < 1.21rc2 < 1.21.0 < 1.21.1 < 1.21.2。

在Go 1.21之前,Go初始发布版本为1.N,而不是1.N.0,因此对于N < 21,排序被调整为将1.N放在候选版本(rc)之后,例如:

1.20rc1 < 1.20rc2 < 1.20rc3 < 1.20 < 1.20.1。

更早期版本的Go有beta发布,例如1.18beta2。Beta发布在版本排序中被放置在候选版本之前,例如:

1.18beta1 < 1.18beta2 < 1.18rc1 < 1.18 < 1.18.1。

有了上述对Go version等的理解,我们再来看看go.mod中go指令在向前兼容性规则中的作用。

Go 1.21及更高版本中,go.mod文件中的go指令声明了使用模块或工作空间(workspace)所需的最低Go版本。出于兼容性原因,如果go.mod文件中省略了go指令行(通常我们都不这么做),则该模块被视为隐式使用go 1.16这个指令行;如果go.work文件中省略了go指令行,则该工作空间被视为隐式使用go 1.18这个指令行。

那么,Go 1.21及更高版本的Go工具链在遇到go.mod中go指令行中的Go版本高于自身时会怎么做呢?下面我们通过四个场景的示例来看一下。

  • 场景一

当前本地工具链go 1.22.0,go.mod中go指令行为go 1.23.0:

// toolchain-directive/demo2/scene1/go.mod
module scene1

go 1.23.0

执行构建:

$go build
go: downloading go1.23.0 (darwin/amd64)
... ...

Go自动下载当前go module中go指令行中的Go工具链版本并对当前module进行构建。

  • 场景二

当前本地工具链go 1.22.0,go.mod中go指令行为go 1.22.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1:

// toolchain-directive/demo2/scene2/go.mod
module scene2

go 1.22.0

require (
    github.com/bigwhite/a v1.0.0
) 

replace github.com/bigwhite/a => ../a

执行构建:

$go build
go: module ../a requires go >= 1.23.1 (running go 1.22.0)

Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新,则会输出错误提示!

  • 场景三

当前本地工具链go 1.22.0,go.mod中go指令行为go 1.22.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1,而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2:

// toolchain-directive/demo2/scene3/go.mod
module scene3

go 1.22.0

require (
    github.com/bigwhite/a v1.0.0
    github.com/bigwhite/b v1.0.0
) 

replace github.com/bigwhite/a => ../a
replace github.com/bigwhite/b => ../b

执行构建:

$go build
go: module ../b requires go >= 1.23.2 (running go 1.22.0)

Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新,则会输出错误提示!并且选择了满足依赖构建的最小的Go工具链版本。

  • 场景四

当前本地工具链go 1.22.0,go.mod中go指令行为go 1.23.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1,而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2:

// toolchain-directive/demo2/scene4/go.mod
module scene4

go 1.23.0

require (
    github.com/bigwhite/a v1.0.0
    github.com/bigwhite/b v1.0.0
) 

replace github.com/bigwhite/a => ../a
replace github.com/bigwhite/b => ../b

执行构建:

$go build
go: downloading go1.23.0 (darwin/amd64)
... ..

Go发现当前go module依赖的go module中go指令行中的Go版本与当前module的兼容,但比本地Go工具链版本更新,则会下载当前go module中go指令行中的Go版本进行构建。

从以上场景的执行情况来看,只有选择了当前go module的工具链版本时,才会继续构建下去,如果本地找不到这个版本的工具链,go会自动下载该版本工具链再进行编译(前提是GOTOOLCHAIN=auto)。如果像场景2和场景3那样,依赖的module的最低Go version大于当前module的go version,那么Go会提示错误并结束编译!后续你需要显式指定要使用的工具链才能继续编译!以场景3为例,通过GOTOOLCHAIN显式指定工具链,我们可以看到下面结果:

// demo2/scene3

$GOTOOLCHAIN=go1.22.2 go build
go: downloading go1.22.2 (darwin/amd64)
^C

$GOTOOLCHAIN=go1.23.3 go build
go: downloading go1.23.3 (darwin/amd64)
.. ...

我们看到,go完全相信我们显式指定的工具链版本,即使是不满足依赖module的最低go版本要求的!

想必大家已经感受到支持新向前兼容规则带来的复杂性了!这里我们还没有显式使用到toolchain指令行呢!但其实,在上述场景中,虽然我们没有在go.mod中显式使用toolchain指令行,但Go模块会使用隐式的toolchain指令行,其隐式的默认值为toolchain goV,其中V来自go指令行中的Go版本,比如go1.22.0等。

接下来我们就简单地看看toolchain指令行,我们的宗旨是尽量让事情变简单,而不是变复杂!

3. toolchain指令行与GOTOOLCHAIN

Go mod的参考手册告诉我们:toolchain指令仅在模块为主模块且默认工具链的版本低于建议的工具链版本时才有效,并建议:Go toolchain指令行中的go工具链版本不能低于在go指令行中声明的所需Go版本。

也就是说如果对toolchain没有特殊需求,我们还是尽量隐式的使用toolchain,即保持toolchain与go指令行中的go版本一致。

另外一个影响go工具链版本选择的是GOTOOLCHAIN环境变量,它的值决定了go命令的行为,特别是当go.mod文件中指定的Go版本(通过go或toolchain指令)与当前运行的go命令的版本不同时,GOTOOLCHAIN的作用就体现出来了。

GOTOOLCHAIN可以设置为以下几种形式:

  • local: 这是最简单的形式,它指示go命令始终使用其自带的捆绑工具链,不允许自动下载或切换到其他工具链版本。即使go.mod文件要求更高的版本,也不会切换。如果版本不满足,则会报错。

  • \<name> (例如go1.21.3): 这种形式指示go命令使用特定名称的Go工具链。如果系统中存在该名称的可执行文件(例如在PATH环境变量中找到了go1.21.3),则会执行该工具链。否则,go命令会尝试下载并使用名为\<name>的工具链。如果下载失败或找不到,则会报错。

  • auto(或local+auto): 这是默认设置。在这种模式下,go命令的行为最为智能。它首先检查当前使用的工具链版本是否满足go.mod文件中go和toolchain指令的要求。如果不满足,它会根据如下规则尝试切换工具链。

- 如果go.mod中有toolchain行且指定的工具链名称比当前默认的工具链更新,则切换到toolchain行指定的工具链。
- 如果go.mod中没有有效的toolchain行(例如toolchain default或没有toolchain行),但go指令行指定的版本比当前默认的工具链更新,则切换到与go指令行版本相对应的工具链(例如go 1.23.1对应go1.23.1工具链)。
- 在切换时,go命令会优先在本地路径(PATH环境变量)中寻找工具链的可执行文件,如果找不到,则会下载并使用。
  • \<name>+auto: 这种形式与auto类似,但它指定了一个默认的工具链\<name>。go命令首先尝试使用\<name>工具链。如果该工具链不满足go.mod文件中的要求,它会按照与auto模式相同的规则尝试切换到更新的工具链。这种方式可以用来设定一个高于内置版本的最低版本要求,同时又允许根据需要自动升级。

  • \<name>+path (或local+path): 这种形式与\<name>+auto类似,也指定了一个默认的工具链\<name>。不同之处在于,它禁用了自动下载功能。go命令首先尝试使用\<name>工具链,如果不满足要求,它会在本地路径中搜索符合要求的工具链,但不会尝试下载。如果找不到合适的工具链,则会报错。

大多数情况我们会使用GOTOOLCHAIN的默认值,即在auto模式下。但是如果在国内自动下载go版本不便的情况下,可以使用local模式,这样在本地工具链版本不满足的情况下,可以尽快得到错误。或是通过\<name>强制指定使用特定版本的工具链,这样可以实现对组织内采用的工具链版本的精准控制,避免因工具链版本不一致而导致的问题。

4. 使用go get管理Go指令行和toolchain指令行

自go module诞生以来,我们始终可以使用go get对go module的依赖进行管理,包括添加/删除依赖,升降依赖版本等。

就像本文开头的那个图中所示,go命令作为全方位依赖管理器的角色定位,它不仅管理外部模块,还负责管理Go工具链版本,以及越来越多的外部开发工具。因此我们也可以使用go get管理指令行和toolchain指令行。

例如,go get go@1.22.1 toolchain@1.24rc1将改变主模块的go.mod文件,将go指令行改为go 1.22.1,将toolchain指令行改为toolchain go1.24rc1。我们要保证toolchain指令行中的版本始终等于或高于go指令行中的版本。

当toolchain指令行与go指令行完全匹配时,可以省略和隐含,所以go get go@1.N.P时可能会删除toolchain行。

反过来也是这样,当go get toolchain@1.N.P时,如果1.N.P < go指令行的版本,go指令行也会随之被降级为1.N.P,这样就和toolchain版本一致了,toolchain指令行可能会被删除。

我们也可以通过下面go get命令显式删除toolchain指令行:

$go get toolchain@none

通过go get管理Go指令行和toolchain指令行还会对require中依赖的go module版本产生影响,反之使用go get管理require中依赖的go module版本时,也会对Go指令行和toolchain指令行的版本产生影响!不过这一切都是通过go get自动完成的!下面我们通过示例来具体说明一下。

我们首先通过示例看看go get管理go指令行对require中依赖的Go模块版本的影响。

当你使用go get升级或降级go.mod文件中的go指令行时,go get 会根据新的Go版本要求,自动调整require指令行中依赖模块的版本,以满足新的兼容性要求。比如下面这个升级go版本导致依赖模块升级的示例。

假设你的模块mymodule的go.mod文件内容如下:

module example.com/mymodule

go 1.21.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0
    example.com/moduleB v1.2.0 // 兼容Go 1.21.0
)

example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本都只兼容到Go 1.21.0。

现在,你执行以下命令升级Go版本:

$go get go@1.23.1

go get会将go.mod文件中的go指令行更新为go 1.23.1。同时,它会检查require指令行中的依赖模块,发现example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本可能不兼容Go1.23.1。

假设example.com/moduleA和example.com/moduleB都有更新的版本v1.3.0,且兼容Go 1.23.1,那么go get会自动将require指令行更新为:

module example.com/mymodule

go 1.23.1

require (
    example.com/moduleA v1.3.0 // 兼容Go 1.23.1
    example.com/moduleB v1.3.0 // 兼容Go 1.23.1
)

如果找不到兼容Go 1.23.1 的版本,go get可能会报错,提示无法找到兼容新Go版本的依赖模块。

同理,降低go版本也可能触发require中依赖模块降级。我们来看下面示例:

假设你的模块mymodule的go.mod文件内容如下:

module example.com/mymodule

go 1.23.1

require (
    example.com/moduleA v1.3.0 // 兼容 Go 1.22.0及以上
    example.com/moduleB v1.3.0 // 兼容 Go 1.22.0及以上
)

现在,你执行以下命令降低go版本:

$go get go@1.22.0

执行以上命令后,go.mod文件内容变为:

module example.com/mymodule

go 1.22.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0及以上
    example.com/moduleB v1.2.0 // 兼容Go 1.21.0及以上
)

在这个例子中, go get go@1.22.0命令会将go指令行降级为go 1.22.0, 同时, go get会自动检查所有依赖项, 并尝试将它们降级到与go 1.22.0兼容的最高版本。在这个例子中, example.com/moduleA和example.com/moduleB都被降级到了与go 1.22.0兼容的最高版本。

反过来,使用go get管理require中依赖的Go模块版本时,也会对go指令行产生影响,我们看一个添加依赖导致go指令行版本升级的示例。

假设你的模块mymodule的go.mod文件内容如下:

module example.com/mymodule

go 1.21.0

require (
    example.com/moduleA v1.1.0 // 兼容 Go 1.21.0
)

现在,你需要添加一个新的依赖项example.com/moduleC,而example.com/moduleC的最新版本v1.2.0的go.mod文件中指定了go 1.22.0:

// example.com/moduleC 的 go.mod
module example.com/moduleC

go 1.22.0

require (
    ...
)

你执行以下命令添加依赖:

$go get example.com/moduleC@v1.2.0

go get会发现example.com/moduleC的版本v1.2.0需要 Go 1.22.0,而你的模块当前只兼容Go 1.21.0。因此,go get会自动将你的模块的go.mod文件更新为:

module example.com/mymodule

go 1.22.0

require (
    example.com/moduleA v1.1.0 // 兼容Go 1.21.0
    example.com/moduleC v1.2.0 // 需要Go 1.22.0
)

go指令行被升级到了go 1.22.0,以满足新添加的依赖项的要求。

不过无论如何双向影响,我们只要记住一个原则就够了,那就是go get和go mod tidy命令使go指令行中的Go版本始终保持大于或等于任何所需依赖模块的go指令行中的Go版本

5. 小结

本文深入探讨了Go语言在版本管理和工具链兼容性方面的重要变革,特别是Go 1.21及以后的版本如何强化向前兼容性。在文章里,我强调了向后兼容性和向前兼容性在开发体验中的重要性,以及如何通过go指令和新引入的toolchain指令来管理工具链版本。

通过文中的示例,我展示了如何在不同场景下处理Go模块的兼容性问题,并解释了GOTOOLCHAIN环境变量如何影响工具链选择。最后,我还举例说明了如何通过使用go get命令有效管理Go指令和依赖模块的版本,确保代码的可维护性和稳定性。

不过我们也看到了,为了实现精确的向前兼容,Go引入了不少复杂的规则,短时间内记住这些规则还是有门槛的,我们只能在实践中慢慢吸收和理解。

本文涉及的源码可以在这里下载。

6. 参考资料


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们>将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流
和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾
。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats