这是我参与8月更文挑战的第 9 天,活动详情查看: 8月更文挑战
本文主要分享go语言中的作用域。包的相关内容已经在前边的文章中分享了,所以没有再整理相关的文章,需要了解的话,点这里。里边包含了包的定义、导入、初始化等相关内容
作用域
声明将名字和程序实体关联起来,如一个函数或一个变量。声明的作用域是指用到声明时所声明名字的代码段。不要将作用域和生命周期混淆。声明的作用域是声明在程序文本中出现的区域,它是一个编译时属性。变量的生命周期是变量在程序执行期间能被程序的其他部分所引用的起止时间,它是一个运行时属性
语法块
语法块(block)是由大括号围起来的一个语句序列,比如一个循环体或函数体。在语法块内部声明的变量对块外部不可见。块把声明包围起来,并决定了它的可见性
词法块
将块的概念推广到其它没有显式包含在大括号中的声明代码,将其统称为词法块。包含了全部源代码的词法块,叫做全局快
每一个包、每一个文件、每一个for、if和switch语句,以及switch和select语句中的每一个条件,都是写在词法块里的。显式的写在大括号里的代码块也算是词法块
一个声明的词法块决定了声明的作用域的大小。像int、len和true等内置类型、函数或常量在全局快中声明,对整个程序可见。在包级别的声明,可以被同一个包里的任何文件引用。导入的包是文件级别的,所以他们可以在同一个文件内引用,但是不能在没有另一个import语句的前提下被同一个包中的其它文件中的东西引用。许多声明是局部的,仅可以在同一个函数中或者仅仅是函数的一部分所引用
控制流标签(break、continue和goto使用的标签)的作用域是整个外层的函数。一个程序可以包含多个同名的声明,前提是他们在不同的词法块中。当编译器遇到一个名字的引用时,将从最内层的封闭词法块到全局快寻找其声明。如果没有找到,它会报“undeclared name”错误,如果在内层和外层块都存在这个声明,内层的将先被找到。这种情况下,内层声明将覆盖外部声明,使它不可访问
func f() {}
var g = "g"
func main() {
f := "f"
fmt.Println(f)//"f" 局部变量f覆盖了包级别函数f
fmt.Println(g)//"g" 包级变量
fmt.Println(h)//编译错误,未定义
}
示例
在函数中,词法块可能嵌套的很深,所以一个局部变量声明可能覆盖另一个。很多词法块使用if语句和for循环这类控制流结构构件。下边的示例中,有三个称为x的不同变量声明,因为他们每个声明都出现在不同的词法块中
func main() {
x := "hello!"
for i := 0; i< len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x)//HELLO (每次迭代一个字母)
}
}
}
上边也提到过,不是所有的词法块都对应于显式大括号包围的语句序列,有一些词法块是隐式的。for循环创建了两个词法块:一个是循环体本身的显式块,以及一个隐式块,它包含了一个闭合结构,其中就有初始化语句中声明的变量,如变量i。隐式块中声明的变量的作用域包括条件、后置语句(i++),以及for语句体本身
下边这个例子中,也有三个名为x的变量,每一个都在不同的词法块中声明:一个在函数体中,一个在for语句块中,一个在循环体中。但只有两个块是显式的:
func main() {
x := "hello"
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x)//HELLO (每次迭代一个字母)
}
}
像for循环一样,除了本身的主体块以外,if和switch语句还会创建隐式的词法块。下例中的if-else链展示x和y的作用域
if x := f();x==0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) //编译错误,x和y在这里不可见
第二个if语句嵌套在第一个中,所以第一个语句的初始化部分声明的变量在第二个语句中是可见的。同样的规则可以应用于switch语句:条件对应一个块,每一个case语句体对应一个块
if f, err := os.Open(fname); err != nil {//编译错误,f未使用
return err
}
f.Stat() //编译错误,f未定义
f.Close()//编译错误,f未定义
f的作用域是if语句,所以f不能被接下来的语句访问,编译器会报错(根据编译器的不同,也肯能收到其他的报错:局部变量f没有被使用(在Go语言中,如果一个变量被定义了,却没有使用,是编译不通过的))
对于以上这种情况,通常是在条件判断之前,声明f,使其在if条件语句之后可以访问
f, err := os.Open(fname)
if err != nil {
return err
}
f.Stat()
f.Close()
或者
if f, err := os.Open(fname); err != nil {
return err
} else {
f.Stat()
f.Close()
}
通常Go的做法是在if块中处理错误,然后返回,这样成功执行的路径不会被变得支离破碎
参考
《Go程序设计语言》—-艾伦 A. A. 多诺万
《Go语言学习笔记》—-雨痕