Go 1.22引入的包级变量初始化次序问题

细心的朋友可能已经注意到,从春节后,我的博客就“停更”了!实际上,这一情况部分是因为工作上的事务繁忙,另一部分则是因为我将工作之外的闲暇时间更多地投入到一本即将于今年中下旬出版的书的撰写了:在之前的积累基础上,我花了两个多月的时间完成了初稿。

当然,我也深切地怀念博客写作所带来的乐趣和与读者的互动。正巧,今天一位学员在《Go语言第一课》专栏[1]留言给了我一个恢复下笔的机会。借此,我也准备恢复一下博客写作的节奏。

另外预告一下:我和我的技术团队合作翻译的一本Go语言入门书最早也将于2024年4月份上市,敬请期待


在《Go语言第一课》专栏[2]的第8讲[3]中,我曾系统讲解了Go包的初始化次序,以及Go包内包级变量、常量、init函数等的初始化次序。讲这些的初衷就是希望Go初学者能先了解一下Go程序的执行次序,这样在后续阅读和理解Go代码的时候,就好比拥有了“通往宝藏的地图”,可以直接沿着Go代码执行次序这张“地图”去阅读和理解Go代码,而不会在庞大的代码库中迷失了。

相对于早期的Go版本,Go包的初始化次序在Go 1.21版本[4]开始会有所变化,这个可以看我的《Go 1.21中值得关注的几个变化》[5]一文了解详情。

不过除了Go包的初始化次序得以明确之外,Go在1.22版本中的包级变量初始化次序也发生了一些“变化”,但Go 1.22的Release Note[6]压根没提到Go包内的变量初始化次序会有变化。究竟这些变化是有意为之,还是由于代码变更而引入的新问题呢?我们还得从近期《Go语言第一课》专栏[7]的一位读者提出的问题讲起!

1. Go 1.22的输出结果与专栏文章中不同!

原专栏中的代码较多,为方便起见我又写了一段简化版的代码,可以等价地反映问题。下面的代码用于演示包级变量、常量和init函数的初始化次序:

// initorder.go
package main

import (
 "fmt"
)

var (
 v0 = constInitCheck()
 v1 = variableInit("v1")
 v2 = variableInit("v2")
)

const (
 c1 = "c1"
 c2 = "c2"
)

func constInitCheck() string {
 if c1 != "" {
  fmt.Println("main: const c1 has been initialized")
 }
 if c1 != "" {
  fmt.Println("main: const c2 has been initialized")
 }
 return ""
}

func variableInit(name string) string {
 fmt.Printf("main: var %s has been initialized\n", name)
 return name
}

func init() {
 fmt.Println("main: first init func invoked")
}

func init() {
 fmt.Println("main: second init func invoked")
}

func main() {
 // do nothing
}

使用Go 1.22版本之前的版本,比如Go 1.21版本[8],运行该程序的输出结果如下:

$go run initorder.go
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked

这个输出结果也是专栏文章中的输出结果,即包级元素的初始化顺序是:常量 -> 变量 -> init函数。三个变量的初始化次序是v0 -> v1 -> v2。

但专栏的一位读者在使用最新Go 1.22版本运行上述程序后,却提出了如下问题:

4ec34ef3feb8fdbf9735e62423b93c3a.png

总结一下这个问题的两个关键点如下:

  • Go 1.22版本运行上述程序的输出结果与文章中的结果不一致

  • 将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果与文章中的一致

我们先来复现一下问题。我使用Go 1.22.0运行上面的initorder.go,得到下面结果:

$go run main.go
main: var v1 has been initialized
main: var v2 has been initialized
main: const c1 has been initialized
main: const c2 has been initialized
main: first init func invoked
main: second init func invoked

该输出结果确如读者所说,与文中的输出顺序不一致了,变量的初始化次序变为了v1 -> v2 -> v0。这会让很多读者误以为包内元素的初始化次序变成了“变量 -> 常量 -> init函数”。是否真的如此了呢?我们下面来初步分析一下。

2. 原因初步分析

Go语言规范[9]中对包内变量初始化次序的说明[10]是这样的(截至2024.03):

Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables. More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization. Multiple variables on the left-hand side of a variable declaration initialized by single (multi-valued) expression on the right-hand side are initialized together: If any of the variables on the left-hand side is initialized, all those variables are initialized in the same step. For the purpose of package initialization, blank variables are treated like any other variables in declarations.

粗略翻译后大致意思如下:

在包内,包级变量初始化逐步进行,每一步都会选择声明顺序中最早的且不依赖于未初始化变量的那个变量。更准确地说,如果包级变量尚未初始化并且没有初始化表达式或其初始化表达式不依赖于未初始化的变量,则认为该变量具备初始化条件。通过重复初始化声明顺序中最早且具备初始化条件的下一个包级变量来进行初始化,直到没有具备初始化条件的变量为止。由右侧单个(多值)表达式初始化的变量声明左侧的多个变量会一起初始化:如果左侧的任何变量被初始化,则所有这些变量都会被初始化在同一步骤中。出于包初始化的目的,空变量也被视为与声明中的任何其他变量一样。

按照Go语言规范的描述,我们来理论推导一下v0、v1和v2的初始化次序:

var (
    v0 = constInitCheck()
    v1 = variableInit("v1")
    v2 = variableInit("v2")
)
  • 第一轮:待初始化的包级变量集合{v0, v1, v2}。在这一轮,我们按声明顺序逐一看一下这三个变量。

v0未初始化,其声明语句的右侧有初始化表达式(initialization expression),且这个初始化表达式式(constInitCheck)不依赖未初始化的变量(仅仅依赖两个常量c1和c2),因此按照Spec描述,v0具备初始化条件,会先进行初始化,于是constInitCheck会被调用。

  • 第二轮:待初始化的包级变量集合{v1, v2}。

按声明顺序,先看v1。和v0一样,其声明语句的右侧有初始化表达式,且这个初始化表达式式(variableInit)不依赖未初始化的变量,因此按照Spec描述,v1具备初始化条件,会进行初始化,于是variableInit会被调用。

  • 第三轮:待初始化的包级变量集合{v2}。

这个没啥可推导的了,初始化v2就是了!

这样,包级变量的声明次序就应该是v0 -> v1 -> v2。这个理论推导结果显然与Go 1.22版之前的输出结果是一致的。但与Go 1.22版本的输出结果有悖。

那么Go 1.22版本为什么没有将v0作为第一个具备初始化条件的变量对其进行初始化呢?v0有初始化表达式constInitCheck,该函数没有依赖任何未初始化的包级变量,但该函数内部依赖了两个常量c1和c2:

func constInitCheck() string {
    if c1 != "" {
        fmt.Println("main: const c1 has been initialized")
    }
    if c1 != "" {
        fmt.Println("main: const c2 has been initialized")
    }
    return ""
}

我们大胆地猜测一下:Go 1.22版本将c1和c2当成了“未初始化的变量”了!还记得读者问题的第二个关键点吗:“将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果便与文章中的一致”。按照Go 1.22的逻辑,将常量声明放到前面后,按顺序常量先被初始化了。这样到v0时,v0具备初始化的条件就成立了,于是v0就可以先被初始化了。

3. “一波三折”的issue

为了证实上述推测,我在github.com/golang/go提了issue 66575[11],并对上述问题做了阐述,不过该issue被Go团队的年轻成员Sean Liao[12]“闪电”关闭了。

好在几个小时后,Go大神Keith Randall[13]看到了这个issue,并支持了我的猜测!他还闪电般地找出了导致Go 1.22版本出现此问题的commit[14],并给出了fix方案:cmd/compile: put constants before variables in initialization order[15]。fix方案的思路就是将所有常量的初始化放到变量之前。

该fix merge到主干[16]后,Gobot自动关闭了该issue。

但严谨的Keith Randall随后reopen了该issue,并圈了Go语言之父的Robert Griesemer[17],希望后者确定一下是否需要更新一下Go spec。

目前该issue已经被加入Go 1.23 milestone[18],并会在Go 1.23 fix。


Gopher部落知识星球[19]在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

56d3aed4ab11a19a82c2e3ea55510551.jpeg7fd670188000602d1192cfec4536c891.png

f1a63fbf41f863cac07a0722d477f2dc.pngcc792fbe48ff15afac56d7768f31e5c8.jpeg

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[20]: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

9f177cab47c4db9c507d6067c63baaf9.jpeg

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

参考资料

[1] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[2] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[3] 

第8讲: https://time.geekbang.org/column/article/432021

[4] 

Go 1.21版本: https://go.dev/doc/go1.21

[5] 

《Go 1.21中值得关注的几个变化》: https://tonybai.com/2023/08/20/some-changes-in-go-1-21

[6] 

Go 1.22的Release Note: https://go.dev/doc/go1.22

[7] 

《Go语言第一课》专栏: http://gk.link/a/10AVZ

[8] 

Go 1.21版本: https://tonybai.com/2023/08/20/some-changes-in-go-1-21/

[9] 

Go语言规范: https://go.dev/ref/spec

[10] 

包内变量初始化次序的说明: https://go.dev/ref/spec#Package_initialization

[11] 

issue 66575: https://github.com/golang/go/issues/66575

[12] 

Sean Liao: https://github.com/seankhliao

[13] 

Keith Randall: https://github.com/randall77

[14] 

导致Go 1.22版本出现此问题的commit: https://go-review.googlesource.com/c/go/+/517617

[15] 

fix方案:cmd/compile: put constants before variables in initialization order: https://go-review.googlesource.com/c/go/+/575075

[16] 

fix merge到主干: https://github.com/golang/go/commit/8f618c1f5329bd81912f8f776cb8bf028c750687

[17] 

Robert Griesemer: https://github.com/griesemer

[18] 

Go 1.23 milestone: https://github.com/golang/go/milestone/212

[19] 

Gopher部落知识星球: https://public.zsxq.com/groups/51284458844544

[20] 

链接地址: https://m.do.co/c/bff6eed92687

  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 您好!如果您想要下载Kubernetes版本1.22的tar包,可以按照以下步骤进行操作: 首先,您可以访问Kubernetes官方的GitHub页面(https://github.com/kubernetes/kubernetes/releases)来获取最新版本的Kubernetes源代码。 在该页面上,您可以找到所有可用的Kubernetes版本,包括1.22版本。通过点击相应的版本号,您会进入该版本的发布页面。 在版本的发布页面上,您会看到有关该版本的详细信息和发布说明。请仔细阅读该信息,以确保该版本符合您的需求。 接下来,在发布页面的“Assets”部分,您将找到各种可下载的文件和工具。在这里,您可以找到以“tar.gz”为扩展名的tar包文件。 点击您想要下载的tar包文件链接,您将会进入下载页面。在这里,您可以选择下载该文件的方式,比如使用HTTP或者使用Git克隆。 选择适合您的下载方式后,点击相应的链接,即可开始下载kube1.22.tar.gz文件。 需要注意的是,由于Kubernetes是一个开源项目,您也可以选择从其他来源下载Kubernetes的tar包。但无论从哪里下载,建议您始终从官方渠道获取文件,以确保其安全性和完整性。 希望以上信息对您有所帮助!如果还有其他问题,请随时提问。 ### 回答2: kube1.22.tar包是Kubernetes 1.22版本的压缩文件,可以通过以下步骤进行下载: 1. 打开您的网页浏览器,进入Kubernetes官方网站(https://kubernetes.io/zh/)。 2. 在官网的顶部导航栏中,找到并点击"下载"或"Downloads"选项。 3. 在下载页面中,您可以在"kubectl"下方找到不同版本的Kubernetes软件包,包括kube1.22.tar。 4. 点击kube1.22.tar下载链接,您将被引导到一个新的页面。 5. 在该页面中,您可以看到有关kube1.22.tar的详细信息和下载链接。 6. 点击下载链接,浏览器将开始自动下载kube1.22.tar文件。 7. 请等待下载完成,下载速度取决于您的网络连接和文件大小。 8. 下载完成后,您可以在您的计算机的默认下载目录中找到kube1.22.tar文件。 要点: - 在Kubernetes官方网站上可以找到kube1.22.tar的下载链接。 - 点击下载链接,浏览器将自动下载kube1.22.tar文件。 - 下载完成后,您可以在默认下载目录中找到kube1.22.tar文件。 ### 回答3: kube1.22.tar包是Kubernetes(K8s)的一个发行版本。它是以tar包的形式提供,用于快速、简便地下载和安装Kubernetes。Kubernetes是一个开源的容器编排平台,用于自动化应用程序的部署、扩展和管理。它提供了一系列功能,包括容器编排、服务发现、负载均衡、自动伸缩等,帮助用户更好地管理容器化的应用程序。 要下载kube1.22.tar包,可以通过以下步骤进行操作: 1. 打开Kubernetes官方网站(https://kubernetes.io/)。 2. 导航到“下载”或类似的页面,找到适用于您系统的kube1.22.tar包下载链接。 3. 点击下载链接,开始下载kube1.22.tar包。 4. 下载完成后,您可以将tar包保存在本地的任意目录。 一旦下载完成,您可以通过以下步骤安装Kubernetes: 1. 解压tar包。您可以使用命令行工具执行以下命令:`tar -xvf kube1.22.tar`。 2. 进入解压后的目录:`cd kube1.22`。 3. 根据您的操作系统和需要,执行适当的安装脚本。例如,如果您使用的是Linux系统,则可以执行`./install.sh`来启动安装过程。根据安装脚本的指导,您可能需要提供一些配置选项,比如Master节点和Worker节点的IP地址。 4. 完成安装过程后,您可以验证Kubernetes的安装是否成功,可以执行`kubectl version`命令来检查Kubernetes的版本信息。 总之,通过下载kube1.22.tar包,并按照相应的安装步骤进行操作,即可安装和使用Kubernetes平台。这将为您提供一个强大的容器编排工具,帮助您更有效地管理和扩展容器化应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值