Go1.20 将会修改全局变量的初始化顺序。梅度二开,继续打破 Go1 兼容性承诺!...

本文主要是介绍Go1.20 将会修改全局变量的初始化顺序。梅度二开,继续打破 Go1 兼容性承诺!...,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,我是煎鱼。

Go1.20 已经发布了 rc1,大家都关注了一些大头的功能特性,例如:PGO、Arean 等,都没有那么的常接触到。

实质上本次新版本还修复了在全局变量初始化方面的顺序,来自《cmd/compile: global variable initialization done in unexpected order[1]》,这是个挺有趣的问题。

神奇案例

从案例展开,假设在同一个 package 下有 2 个文件,分别是:f1.go 和 f2.go,包含了不同的包全局变量声明和代码。

文件 f1.go。代码如下:

package main    var A int = 3    
var B int = A + 1    
var C int = A

文件 f2.go。代码如下:

package main    import "fmt"    var D = f()      func f() int {    A = 1    return 1    
}    func main() {    fmt.Println(A, B, C)    
}

问题来了。

如果运行 go run f1.go f2.go,会输出什么结果?

运行结果如下:

1 4 3

你答对了吗?再仔细想想。

如果运行 go run f2.go f1.go,会输出什么结果?

运行结果如下:

1 2 3

这只是 run 的文件先后顺序不一样了,咋就连输出的结果都不一样了?

输出结果到底谁对谁错,还是说都错了,正确的是什么?

Go 规范定义

我们要知道正确输出的结果是什么,还得是看 Go 语言规范《The Go Programming Language Specification[2]》说了算。

0e2018cce5259967dc33fc34819a5b33.png
sepc

在规范中的包初始化(Package initialization)章节中明确指出:"在一个包中,包级别的变量初始化是逐步进行的,每一步都会选择声明顺序中最早的变量,它不依赖于未初始化的变量。"

更完整和准确的阐述:

  • 如果包级变量尚未初始化并且没有初始化表达式或其初始化表达式不依赖于未初始化的变量,则认为包级变量已准备好进行初始化。

  • 初始化通过重复初始化声明顺序中最早并准备初始化的下一个包级变量来进行,直到没有变量准备好进行初始化。

在了解了理论知识后,我们再结合官方例子看看,加强实践的补全。

例子 1。代码如下:

var x = a
var a, b = f()

在初始化变量 x 之前,变量 a 和 b 会一起初始化(在同一步骤中)。

例子 2。代码如下:

var (a = c + b  // == 9b = f()    // == 4c = f()    // == 5d = 3      // == 5 after initialization has finished
)func f() int {d++return d
}

初始化顺序是:d, b, c, a。

案例哪里有问题

在解读了背景和规范后,再次回顾文章刚开始的案例。

文件 f1.go。代码如下:

package main    var A int = 3    
var B int = A + 1    
var C int = A

文件 f2.go。代码如下:

package main    import "fmt"    var D = f()      func f() int {    A = 1    return 1    
}    func main() {    fmt.Println(A, B, C)    
}

第一种,运行 go run f1.go f2.go,输出:1 4 3。

第二种,运行 go run f2.go f1.go,输出:1 2 3.

如果按照规范来,分析程序变量初始化顺序和应该输出的结果。如下:

  • 第一种的顺序是 A < B < C < D:发生在你编译项目时,运行命令先把 f1.go 传给编译器,然后再传 f2.go。在这种情况下,输出结果是 1 4 3。

  • 第二种的顺序是 A < D < B < C:发生在先将 f2.go 传给编译器时。在这种情况下,预期输出是 1 2 1。然而,实际的输出是 1 2 3。

问题出在第二种情况,我们尝试改一下写法,变成如下代码:

package main    import "fmt"    var A int = initA()    
var B int = initB()    
var C int = initC()    func initA() int {    fmt.Println("Init A")    return 3    
}    func initB() int {    fmt.Println("Init B")    return A + 1    
}    func initC() int {    fmt.Println("Init C")    return A    
}

输出结果:

Init A
Init B
Init C
1 2 1

预期结果就一致了。

上一个案例输出 1 2 3,这是有 BUG!与 Go 规范定义的不一致。

修复时间

目前这个问题已经明确是 Go 编译/运行时的 BUG,并且这个问题已经存在了很久,将计划在 Go1.20 中修复。

不过由于不知道是否会影响用户,因此 Go 官方将会更多的关注社区反馈。

当然,这个确实是 BUG,会修。也为此认为值得打破 Go1 兼容性的原则。

总结

今天这篇文章我们介绍了 Go 一直以来存在的一个 Go 编译/运行时的 BUG,会导致 Go 程序的全局变量会与 Go 规范本身定义的不一致,将预计会在 Go1.20 修复。

这也是 Go 打破 Go1 兼容性承诺的又一个案例。值得我们关注。

推荐阅读
  • Go1.20 中两个关于 Time 的更新,终于不用背 2006-01-02 15:04:05 了!

  • 打脸了兄弟们,Go1.20 arena 来了!

  • Go 十年了,终于想起要统一 log 库了!

参考资料

[1]

cmd/compile: global variable initialization done in unexpected order: https://github.com/golang/go/issues/51913

[2]

The Go Programming Language Specification: https://go.dev/ref/spec#Package_initialization

关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇

dac9895d0efe1023a6273df9c067e582.jpeg

11402ad862fc7d50eb40c87dc06b8155.png

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

这篇关于Go1.20 将会修改全局变量的初始化顺序。梅度二开,继续打破 Go1 兼容性承诺!...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/312552

相关文章

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

Python中局部变量和全局变量举例详解

《Python中局部变量和全局变量举例详解》:本文主要介绍如何通过一个简单的Python代码示例来解释命名空间和作用域的概念,它详细说明了内置名称、全局名称、局部名称以及它们之间的查找顺序,文中通... 目录引入例子拆解源码运行结果如下图代码解析 python3命名空间和作用域命名空间命名空间查找顺序命名空

Docker镜像修改hosts及dockerfile修改hosts文件的实现方式

《Docker镜像修改hosts及dockerfile修改hosts文件的实现方式》:本文主要介绍Docker镜像修改hosts及dockerfile修改hosts文件的实现方式,具有很好的参考价... 目录docker镜像修改hosts及dockerfile修改hosts文件准备 dockerfile 文

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Linux修改pip和conda缓存路径的几种方法

《Linux修改pip和conda缓存路径的几种方法》在Python生态中,pip和conda是两种常见的软件包管理工具,它们在安装、更新和卸载软件包时都会使用缓存来提高效率,适当地修改它们的缓存路径... 目录一、pip 和 conda 的缓存机制1. pip 的缓存机制默认缓存路径2. conda 的缓

Linux修改pip临时目录方法的详解

《Linux修改pip临时目录方法的详解》在Linux系统中,pip在安装Python包时会使用临时目录(TMPDIR),但默认的临时目录可能会受到存储空间不足或权限问题的影响,所以本文将详细介绍如何... 目录引言一、为什么要修改 pip 的临时目录?1. 解决存储空间不足的问题2. 解决权限问题3. 提

Linux文件名修改方法大全

《Linux文件名修改方法大全》在Linux系统中,文件名修改是一个常见且重要的操作,文件名修改可以更好地管理文件和文件夹,使其更具可读性和有序性,本文将介绍三种在Linux系统下常用的文件名修改方法... 目录一、引言二、使用mv命令修改文件名三、使用rename命令修改文件名四、mv命令和rename命

Spring组件初始化扩展点BeanPostProcessor的作用详解

《Spring组件初始化扩展点BeanPostProcessor的作用详解》本文通过实战案例和常见应用场景详细介绍了BeanPostProcessor的使用,并强调了其在Spring扩展中的重要性,感... 目录一、概述二、BeanPostProcessor的作用三、核心方法解析1、postProcessB

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu