Go语言学习指南:惯例模式与编程实践
上QQ阅读APP看书,第一时间看更新

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中添加shadowvet任务:

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

全局代码块

在Go语言中有一个很独特的代码块类型:全局代码块。Go语言是一种简单精巧的编程语言,仅有25个关键字。有趣的是,内置的类型(如intstring)、常量(如truefalse)和函数(如makeclose)既不是关键字也不是nil,所以Go是如何定义它们的呢?

Go没有采用关键字定义它们,而是在全局代码块中将它们定义为预声明标识符,包含程序中的所有代码块[2]

因为这些名字在全局代码块中声明,所以它们一样可能会在其他作用域中被覆盖。可以在The Go Playground(https://oreil.ly/eoU2A)中运行示例4-4,看看会发生什么。

示例4-4:覆盖true

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

一定要谨慎定义常量、变量等名称标识符,避免与在全局代码块中的预声明标识符重名。如果无意中出现了这样的错误,运气好程序会编译失败;运气不好,则会出现无法预计和难以查找的问题。

我们当然希望这种具有潜在破坏性的代码能被lint工具检测到,但事实并非如此,覆盖全局代码标识符的影子变量无法被检测出来。


[1]包代码块是指包中除了import导入声明语句以外的所有代码部分。

[2]一个程序运行时只有一个全局代码块,它作为根代码块,这样在全局代码块声明的标识符就可以在任意代码块中使用。