近期,Go team的David CrawShaw在twitter上贴出了一段代码,如下:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_代码块

 

David CrawShaw想表达的意图是gopher们很少在”else if”后面的simple statement中使用“短变量声明”形式,而这段代码是个例外。我们看到b、c两个变量都是在else if 的simple statement中使用短变量声明形式定义的。

 

我个人看到这段代码后,第一反应是:这段代码能编译运行吗?else语句中的“println(a, b, c)”是否会被compiler报出:undefined b, c的错误呢?不知道是否有其他的gopher们与我有同样的反应:)。无论怎样,既然有了疑问,我们就应该把它分析清楚。

 

一. Go代码块和作用域简介

 

根据Go语言的规范,我们知道Go的标识符作用域是基于代码块(code block)的。代码块就是包裹在一对大括号内部的声明和语句,并且是可嵌套的。我们在代码中直观可见的显式的(explicit)code block有很多,比如:函数的函数体、for循环的循环体等:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_变量声明_02

 

但除了显式explict的code block,Go语言中还有几种隐式的(implicit)代码块,它们都是什么呢?这里摘录下go spec原文(不翻译了):

 

1. The universe block encompasses all Go source text.

2. Each package has a package block containing all Go source text for that package.

3. Each file has a file block containing all Go source text in that file.

4. Each "if", "for", and "switch" statement is considered to be in its own implicit block.

5. Each clause in a "switch" or "select" statement acts as an implicit block.

我们看到if语句会引入一个隐式的code block,这为我们后续的分析奠定了基础。

 

二. if语句的code block

 

那么if语句的code block详细情况如何呢?我们分门别类地简单看看:

 

1. if _ 型

 

我们使用最多的if语句类型就是单if型,即:

 

if simplestmt; expression {

    ... ...

}

 

在这种类型的if语句中,有两个code block:一个隐式的code block和一个显式的code block。我们把上面的形式代码做一个等价变化,并加上code block起始和结束点的标注,结果如下:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_if语句_03

 

我们看到if后面的”大括号对”引入的explict code block嵌套在if simplestmt所在的implicit code block内部,这也是为何simplestmt中用短声明形式定义的变量在explict block中可以使用的原因:

 

func main() {

    if a := 1; true {

        fmt.Println(a) // output: 1

    }

}

 

2. if _ else _ 型

 

我们再来看看if _ else _ 型:

 

if simplestmt; expression {

    ... ...

} else {

    ... ...

}

 

分析逻辑同上,我们将上面的伪代码做一个等价变换,并作出code block起始结束点标注:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_显式_04

 

我们看到if _ else _ 型 有三个code block,除了单if型的两个block外,还由else引入一个explict code block(即上面代码中的explict block2)。

 

3. if _ else if _ else _ 型

 

最后我们来看看最为复杂的if _ else if _ else _ 型:

 

if simplestmt1; expression1 {

    ... ...

} else if simplestmt2; expression2 {

    ... ...

}  else {

    ... ...

}

 

我们依旧将上面的伪代码做一个等价变换,并作出code block起始结束点标注,结果如下:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_if语句_05

 

我们看到在该类型下,我们一共识别出两个implict block和四个explict block。

 

三. 对David CrawShaw贴出的那段代码的分析

 

有了第二节的基础,再来看David CrawShaw的这段代码:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_代码块

 

依照我们的思路,我们可以对这段代码做一个等价变化:

 

对一段有关Go Code Block和变量作用域的代码的简要分析_变量声明_07

 

展开后的语句就很是一目了然了,不用说什么大家也会很清楚了。最重要的一点是原来代码中最后的那个else实际上是与最内层的else if配对的,而不是与最开始的if配对的,因此println(a, b, c)的时候,a, b, c三个变量都是已经声明定义了的(在外层的code block中)。

 

对于此类涉及code block或变量作用域的问题,还可以通过go vet -shadow工具来辨别,或通过go run执行后的出错信息来辨别,这里就不详细说明了。