很多Gopher喜欢玩Go
quiz(小测验)游戏,这种小测验考察的是大家对语言的理解深度,回答者不仅要给出程序输出结果,还要说明为什么会输出这样的结果。
在本条中,我们也先来玩一个Go quiz,下面是这个quiz的代码:
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
println(a, b, c)
}
}
有两个答案选项:
A:1 2 3
B:无法通过编译
当我第一次看到这段代码时,第一印象倾向于选择B:这段代码能编译运行吗?对最后的else分支语句中的println(a, b, c)语句,编译器是否会报出“undefined b”之类的错误呢?
但真正执行一遍后,发现A选项才是正确的。于是我带着疑问对这段代码进行了分析,得出一个结论:只有深入了解了Go代码块与作用域规则,才能理解这段代码输出“1 23”的真正原因。
本条我们就围绕上述例子来理解一下Go代码块(code block)和作用域(scope)规则,理解这些规则将有助于我们编写出正确且可读性高的代码。
Go语言中的代码块是包裹在一对大括号内部的声明和语句,且代码块支持嵌套。如果一对大括号之间没有任何语句,那么称这个代码块为空代码块。
代码块是代码执行流流转的基本单元,代码执行流总是从一个代码块跳到另一个代码块。
Go语言中有两类代码块,一类是我们在代码中直观可见的由一堆大括号包裹的显式代码块,比如函数的函数体、for循环的循环体、if语句的某个分支等:
func Foo() {
// 这里是显式代码块,包裹在函数的函数体内
// ...
for {
// 这里是显式代码块,包裹在for循环体内
// 该代码块也是嵌套在函数体显式代码块内部的代码块
// ...
}
if true {
// 这里是显式代码块,包裹在if语句的true分支内
// 该代码块也是嵌套在函数体显式代码块内部的代码块
// ...
}
}
另一类则是没有大括号包裹的隐式代码块。Go规范定义了如下几种隐式代码块。
● 宇宙(Universe)代码块:所有Go源码都在该隐式代码块中,就相当于所有Go代码的最外层都存在一对大括号。
● 包代码块:每个包都有一个包代码块,其中放置着该包的所有Go源码。
● 文件代码块:每个文件都有一个文件代码块,其中包含着该文件中的所有Go源码。
● 每个if、for和switch语句均被视为位于其自己的隐式代码块中。
● switch或select语句中的每个子句都被视为一个隐式代码块。
Go标识符的作用域是基于代码块定义的,作用域规则描述了标识符在哪些代码块中是有效的。下面是标识符作用域规则。
● 预定义标识符,make、new、cap、len等的作用域范围是宇宙块。
● 顶层(任何函数之外)声明的常量、类型、变量或函数(但不是方法)对应的标识符的作用域范围是包代码块。比如:包级变量、包级常量的标识符的作用域都是包代码块。
● Go源文件中导入的包名称的作用域范围是文件代码块。
● 方法接收器(receiver)、函数参数或返回值变量对应的标识符的作用域范围是函数体(显式代码块),虽然它们并没有被函数体的大括号所显式包裹。
● 在函数内部声明的常量或变量对应的标识符的作用域范围始于常量或变量声明语句的末尾,止于其最里面的那个包含块的末尾。
● 在函数内部声明的类型标识符的作用域范围始于类型定义中的标识符,止于其最里面的那个包含块的末尾,见下面的代码示例。
func Foo() {
{ // 代码块1
// 代码块1是包含类型bar标识符的最里面的那个包含代码块
type bar struct {} // 类型标识符bar的作用域始于此
{ // 代码块2
// 代码块2是包含变量a标识符的最里面的那个包含代码块
a := 5 // a的作用域始于此
{
//...
}
// a的作用域止于此
}
// 类型标识符bar的作用域止于此
}
}
代码块和作用域规则为我们分析代码奠定了基础。
要想对本条开始的Go quiz进行分析,我们就需要整体了解一下if条件控制语句的代码
块分布规则。接下来就来看看三种类型if条件语句的代码块情况。
if SimpleStmt; Expression {
...
}
根据代码块规则,if语句自身在一个隐式代码块中,因此单if类型的控制语句中有两
个代码块:一个隐式代码块和一个显式代码块。为了方便理解,我们对上面的代码进行一
个等价变换,并加上代码块起始点和结束点的标注。
{ // 隐式代码块开始
SimpleStmt
if Expression { // 显式代码块开始
...
} // 显式代码块结束
} // 隐式代码块结束
我们看到if后面的大括号对所包裹的显式代码块嵌套在if SimpleStmt所在的隐式代码块内部,这也是为何SimpleStmt中使用短变量声明形式定义的变量可以在if语句的显式代码块中使用。
我们再用例子来说明一下:
func Foo() {
if a := 1; true {
fmt.Println(a)
}
}
等价变换为:
func Foo() {
{
a := 1
if true {
fmt.Println(a)
}
}
}
在等价变换后的代码中,根据上面Go标识符作用域规则中关于函数体内变量标识符作用域的描述,变量a的作用域范围可延伸到if内的显式代码块中,因此在if的显式代码块中使用a是合法的。
2. if { } else { }型
我们再来看看if {} else {}型控制语句的代码块的分布:
if Simplestmt; Expression {
...
} else {
...
}
分析逻辑同上面的if型:对上面的伪代码进行一个等价变换并给出代码块起始点和结
束点的标注。
{ // 隐式代码块开始
SimpleStmt
if Expression { // 显式代码块1开始
...
// 显式代码块1结束
} else { // 显式代码块2开始
...
} // 显式代码块2结束
} // 隐式代码块结束
我们看到if {} else {}型控制语句有三个代码块,除了单if型的两个代码块外,还有一个由else引入的显式代码块(显式代码块2)。同样,用一个例子来说明一下:
func Foo() {
if a,b := 1, 2; false{
fmt.Println(a)
} else {
fmt.Println(b)
}
}
等价变换为:
func Foo() {
{
a, b := 1, 2
if false {
fmt.Println(a)
} else {
fmt.Println(b)
}
}
}
我们看到,在SimpleStmt中声明的变量,其作用域范围可以延伸到else后面的显式代码块中。
if SimpleStmt1; Expression1 {
...
} else if SimpleStmt2; Expression2 {
...
} else {
...
}
我们对上面的伪代码进行等价变换,并作出代码块起始点和结束点的标注,结果如
下:
{ // 隐式代码块1开始
SimpleStmt1
if Expression1 { // 显式代码块1开始
...
} else { // 显式代码块1结束;显式代码块2开始
{ // 隐式代码块2开始
SimpleStmt2
if Expression2 { // 显式代码块3开始
...
} else { // 显式代码块3结束;显式代码块4开始
...
} // 显式代码块4结束
} // 隐式代码块2结束
} // 显式代码块2结束
} // 隐式代码块1结束
在该类型下,我们一共识别出2个隐式代码块和4个显式代码块。
有了上述的规则和分析铺垫,我们再来看看本条开头处的那个Go quiz:
package main
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
println(a, b, c)
}
}
这是一个if {} else if {} else {} 型控制语句的应用。依照我们的分析思路,可以
对这段代码进行等价变换:
func main() {
{
a := 1
if false {
} else {
{
b := 2
if false {
} else {
{
c := 3
if false {
} else {
println(a, b, c)
}
}
}
}
}
}
}
展开后的代码让一切都一目了然了。我们看到a、b、c三个变量都位于不同层次的隐式
代码块中,根据这三个变量的作用域范围,在最深层的else显式代码块中使用变量a、b、c
都是合法的,a、b、c三个变量的值此时就是它们的初值,于是这个Go quiz的输出结果为12 3。