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

大家好,我是煎鱼。

Go1.20 发布在即,大家都关注了一些大头的功能特性,例如: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]》说了算。

sepc

在规范中的包初始化(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/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

推荐阅读

参考资料

[1]

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

[2]

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

发布于 2022-12-15 12:44・IP 属地广东