
4.1 代码块
Go中可以在很多地方声明变量,在不同的地方声明变量具有不同的作用。在函数外声明的变量可以作为函数的参数;在函数内部声明的变量是函数的局部变量。
到目前为止,我们仅仅编写过一种函数,即程序的主(
main
)函数,下一章我们将学习如何声明和调用函数。
每个使用声明的区域称为代码块,在函数之外声明变量、常量、类型和函数的区域称为包代码块。在前面的学习中,我们在程序中使用了import
关键字来访问打印和数学函数(详见第9章)[1],文件代码块包含一个源文件中的所有代码,包括此文件中的包导入声明。在函数中,函数体中顶层的所有变量(包括函数的参数)都在一个代码块中,每对大括号定义一个块,稍后我们将看到Go的流程控制中各自定义了代码块。
在代码块内部可以访问代码块外部定义的变量(通过变量标识符访问)。但是这里引出一个问题:当代码块外部有与代码块内部同名的变量标识符时会发生什么呢?答案是代码块外部的变量将被覆盖。
4.1.1 变量覆盖
在解释什么是变量覆盖之前,我们先来看看示例4-1的代码。你可以直接在The Go Playground(https://oreil.ly/50t6b)上运行代码。
示例4-1:变量覆盖

在运行代码之前,可以尝试猜猜运行的打印结果是什么?
- 没有任何打印,代码编译错误。
- 第一个打印是10,第二个打印是5,第三个打印是5。
- 第一个打印是10,第二个打印是5,第三个打印是10。
下面是正确打印的结果:

影子变量是指这个变量在其代码块外部拥有同名的变量。只要影子变量存在,就无法在代码块内部访问被其覆盖的变量。
在上面的代码中,我们的原意不是想在if
语句内创建一个全新的x
,而是打算将5赋值给最开始声明的变量x
。在if
语句块中的第一个fmt.Println
中,我们可以访问在main函数最外层声明的x
,但是在下一行中,我们在if
语句块内声明新变量x
(并赋值5),并覆盖了来自main
函数最外层声明的x
。在if
语句块中的第二个fmt.Println
中,当访问x
时,由于原变量x
被影子变量覆盖,所以实际访问了新的影子变量x
,其值是5。当执行到if
语句代码块外后,此时变量x
结束了覆盖,接下来在第三个fmt.Println
打印的x
就是我们在函数最开始的地方声明的变量,所以x
的值是10。值得注意的是在这个过程中,x
没有消失也没有被重新赋值,只是在被覆盖的代码块内无法被访问。
我们在第2章曾经介绍过避免使用:=
给已经声明的变量赋值的情况,因为变量容易被意外地覆盖而变成影子变量。所以请记住,我们可以使用:=
创建多个变量并赋值,并且:=
左边可以不都是新变量,只要有一个新变量就是合法的。请看示例4-2,你可以直接在The Go Playground(https://oreil.ly/U_m4B)上运行代码。
示例4-2:多赋值的覆盖

结果如下:

尽管在if
代码块外部已有x
的定义,但x
仍然在if
语句中被覆盖。这是因为:=
仅重用在当前块中声明的变量。使用:=
时,请确保左侧没有任何来自当前代码块外部的变量,否则这些变量会被覆盖而成为影子变量。
注意不要覆盖导入的包,更多内容详见第9章。让我们来看看如果在main
函数中声明一个名为fmt
的变量会产生什么结果?请看示例4-3,你可以直接在The Go Playground(https://oreil.ly/CKQvm)上运行代码。
示例4-3:包名覆盖

执行以上代码会报错:

这里的错误不是定义了变量fmt
,而是调用了局部变量fmt
没有的方法,一旦局部变量fmt
被创建,它就在main
函数中将文件块中定义的包名fmt
覆盖,这样在main
函数代码块中就无法直接访问包fmt
。
4.1.2 检测影子变量
考虑到影子变量可能会带来一些不易察觉的bug,所以最好能确保在程序中没有任何变量被覆盖。go vet
或者golangci-lint
都没有提供工具来检测代码中是否含有被覆盖的变量,但你可以将检测加入编译流程中,请使用下面的命令安装shadow
lint工具:

如果编译使用了make
,你可以在Makefile中添加shadow
的vet
任务:

添加完成后,就可以执行命令make vet
来对代码进行检测,当检测到有变量被覆盖后,就会看到类似下面的错误信息:

全局代码块
在Go语言中有一个很独特的代码块类型:全局代码块。Go语言是一种简单精巧的编程语言,仅有25个关键字。有趣的是,内置的类型(如int
和string
)、常量(如true
和false
)和函数(如make
和close
)既不是关键字也不是nil
,所以Go是如何定义它们的呢?
Go没有采用关键字定义它们,而是在全局代码块中将它们定义为预声明标识符,包含程序中的所有代码块[2]。
因为这些名字在全局代码块中声明,所以它们一样可能会在其他作用域中被覆盖。可以在The Go Playground(https://oreil.ly/eoU2A)中运行示例4-4,看看会发生什么。
示例4-4:覆盖true

以上代码的运行结果如下:

一定要谨慎定义常量、变量等名称标识符,避免与在全局代码块中的预声明标识符重名。如果无意中出现了这样的错误,运气好程序会编译失败;运气不好,则会出现无法预计和难以查找的问题。
我们当然希望这种具有潜在破坏性的代码能被lint工具检测到,但事实并非如此,覆盖全局代码标识符的影子变量无法被检测出来。
[1]包代码块是指包中除了import
导入声明语句以外的所有代码部分。
[2]一个程序运行时只有一个全局代码块,它作为根代码块,这样在全局代码块声明的标识符就可以在任意代码块中使用。