大家好,我是煎鱼

Go1.20 发布在即,大家都重视了一些大头的功能特性,例如:PGO、Arean 等。都没有那么的常接触到。

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

奇特事例

从事例打开,假设在同一个 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》说了算。

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

在标准中的包初始化(Package initialization)章节中明确指出:”在一个包中,包等级的变量初始化是逐步进行的,每一步都会挑选声明次序中最早的变量,它不依赖于未初始化的变量。”

更完整和准确的阐述:

  • 假如包级变量尚未初始化而且没有初始化表达式或其初始化表达式不依赖于未初始化的变量,则以为包级变量已预备好进行初始化。
  • 初始化经过重复初始化声明次序中最早并预备初始化的下一个包级变量来进行,直到没有变量预备好进行初始化。

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

比如 1。代码如下:

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

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

比如 2。代码如下:

var (
	a = c + b  // == 9
	b = f()    // == 4
	c = f()    // == 5
	d = 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

预期成果就一致了。

这是有 BUG!与 Go 标准界说的不一致。

修正时间

现在这个问题现已明确是 Go 编译/运转时的 BUG,而且这个问题现已存在了很久,将计划在 Go1.20 中修正。

不过因为不知道是否会影响用户,因而 Go 官方将会更多的重视社区反馈。当然,这个确实是 BUG,会修。也为此以为值得打破 Go1 兼容性的准则。

总结

今日这篇文章咱们介绍了 Go 一直以来存在的一个 Go 编译/运转时的 BUG,会导致 Go 程序的全局变量会与 Go 标准自身界说的不一致,将预计会在 Go1.20 修正。

这也是 Go 打破 Go1 兼容性承诺的一个事例。值得咱们重视。

文章继续更新,可以微信搜【脑子进煎鱼了】阅览,本文 GitHub github.com/eddycjy/blo… 已录入,学习 Go 言语可以看 Go 学习地图和道路,欢迎 Star 催更。

Go 图书系列

  • Go 言语入门系列:初探 Go 项目实战
  • Go 言语编程之旅:深化用 Go 做项目
  • Go 言语规划哲学:了解 Go 的为什么和规划思考
  • Go 言语进阶之旅:进一步深化 Go 源码

推荐阅览

  • Go1.20 中两个关于 Time 的更新,总算不用背 2006-01-02 15:04:05 了!
  • 打脸了兄弟们,Go1.20 arena 来了!
  • Go 十年了,总算想起要一致 log 库了!