2.7 作用域
声明将名字和程序实体关联起来,如一个函数或一个变量。声明的作用域是指用到声明时所声明名字的源代码段。
不要将作用域和生命周期混淆。声明的作用域是声明在程序文本中出现的区域,它是一个编译时属性。变量的生命周期是变量在程序执行期间能被程序的其他部分所引用的起止时间,它是一个运行时属性。
语法块(block)是由大括号围起来的一个语句序列,比如一个循环体或函数体。在语法块内部声明的变量对块外部不可见。块把声明包围起来,并且决定了它的可见性。我们可以把块的概念推广到其他没有显式包含在大括号中的声明代码,将其统称为词法块。包含了全部源代码的词法块,叫作全局块。每一个包,每一个文件,每一个for、if和switch语句,以及switch和select语句中的每一个条件,都是写在一个词法块里的。当然,显式写在大括号语法里的代码块也算是一个词法块。
一个声明的词法块决定声明的作用域大小。像int、len和true等内置类型、函数或常量在全局块中声明并且对于整个程序可见。在包级别(就是在任何函数外)的声明,可以被同一个包里的任何文件引用。导入的包(比如在tempconv例子中的fmt)是文件级别的,所以它们可以在同一个文件内引用,但是不能在没有另一个import语句的前提下被同一个包中其他文件中的东西引用。许多声明(像tempconv.CToF函数中变量c的声明)是局部的,仅可在同一个函数中或者仅仅是函数的一部分所引用。
控制流标签(如break、continue和goto语句使用的标签)的作用域是整个外层的函数(enclosing function)。
一个程序可以包含多个同名的声明,前提是它们在不同词法块中。例如可以声明一个和包级别变量同名的局部变量。或者像2.3.3节展示的,可以声明一个叫作new的参数,即使它是一个全局块中预声明的函数。然而,不要滥用,重声明所涉及的作用域越广,越可能影响其他的代码。
当编译器遇到一个名字的引用时,将从最内层的封闭词法块到全局块寻找其声明。如果没有找到,它会报“undeclared name”错误;如果在内层和外层块都存在这个声明,内层的将先被找到。这种情况下,内层声明将覆盖外部声明,使它不可访问:
在函数里面,词法块可能嵌套很深,所以一个局部变量声明可能覆盖另一个。很多词法块使用if语句和for循环这类控制流结构构建。下面的程序有三个称为x的不同的变量声明,因为每个声明出现在不同的词法块。(这个例子只是用来说明作用域的规则,风格并不完美!)
表达式x[i]和x + 'A' - 'a'都引用了在外层声明的x,稍后我们会解释它。(注意,后面的表达式不同于unicode.ToUpper函数。)
如上所述,不是所有的词法块都对应于显式大括号包围的语句序列,有一些词法块是隐式的。for循环创建了两个词法块:一个是循环体本身的显式块,以及一个隐式块,它包含了一个闭合结构,其中就有初始化语句中声明的变量,如变量i。隐式块中声明的变量的作用域包括条件、后置语句(i++),以及for语句体本身。
下面的例子也有三个名字为x的变量,每一个都在不同的词法块中声明:一个在函数体中,一个在for语句块中,一个在循环体中。但只有两个块是显式的:
像for循环一样,除了本身的主体块之外,if和switch语句还会创建隐式的词法块。下面的if-else链展示x和y的作用域:
第二个if语句嵌套在第一个中,所以第一个语句的初始化部分声明的变量在第二个语句中是可见的。同样的规则可以应用于switch语句:条件对应一个块,每个case语句体对应一个块。
在包级别,声明的顺序和它们的作用域没有关系,所以一个声明可以引用它自己或者跟在它后面的其他声明,使我们可以声明递归或相互递归的类型和函数。如果常量或变量声明引用它自己,则编译器会报错。
在以下程序中:
f变量的作用域是if语句,所以f不能被接下来的语句访问,编译器会报错。根据编译器的不同,也可能收到其他报错:局部变量f没有使用。
所以通常需要在条件判断之前声明f,使其在if语句后面可以访问:
你可能希望避免在外部块中声明f和err,方法是将Stat和Close的调用放到else块中:
通常Go中的做法是在if块中处理错误然后返回,这样成功执行的路径不会被变得支离破碎。
短变量声明依赖一个明确的作用域。考虑下面的程序,它获取当前的工作目录然后把它保存在一个包级别的变量里。这通过在main函数中调用os.Getwd来完成,但是最好可以从主逻辑中分离,特别是在获取目录失败是致命错误的情况下。函数log.Fatalf输出一条消息,然后调用os.Exit(1)退出。
因为cwd和err在init函数块的内部都尚未声明,所以:=语句将它们都声明为局部变量。内层cwd的声明让外部的声明不可见,所以这个语句没有按预期更新包级别的cwd变量。
当前Go编译器检测到局部的cwd变量没有被使用,然后报错,但是不必严格执行这种检查。进一步做一个小的修改,比如增加引用局部cwd变量的日志语句就可以让检查失效。
全局的cwd变量依然未初始化,看起来一个普通的日志输出让bug变得不明显。
处理这种潜在的问题有许多方法。最直接的方法是在另一个var声明中声明err,避免使用:=。
现在我们已经看到包、文件、声明以及语句是如何来构成程序的。接下来的两章将要讨论数据的结构。