前面我们已经讲解了变量、常量和内置类型,下一步要学习程序逻辑和组织方式了。我们会先讲解代码块,以及代码块如何控制某个标识符的可用性。然后我们一起学习Go语言的控制结构:if、for和switch。最后我们会讨论goto,以及使用它的场景。
代码块
Go允许在多处声明变量。可以在函数外声明、声明函数参数也可以在函数内声明本地变量。
注:至此,我们只写过main函数,下一章就会开始写一些带参函数。
每个产生声明的地方称为代码块。函数外声明的变量、常量、类型位于包代码块中。我们已经使用过import 语句来在程序中使用打印和数学函数(在模块、包和导入中还会做进一步讨论)。它们为其它包定义有效名称,可在包含import 语句的文件 中使用。这些名称位于文件代码块中。在函数顶层定义的所有变量(包含函数的参数)都处于这一代码块中。在函数内,每组花括号 ({})定义一个新的代码块,我们稍后会看到Go的控制结构中会定义自己的代码块。
可在内层代码块中访问所有外层代码块中定义的标识符。这就带来了一个问题:在内层代码块中定义相同名称的标识符会怎么样呢?这时就会遮蔽外层所定义的标识符。
变量遮蔽
在讲解什么是遮蔽之前,我们先看一段代码(例 4-1)。可在The Go Playground中运行这段代码。
例 4-1 变量遮蔽
func main() {
x := 10
if x > 5 {
fmt.Println(x)
x := 5
fmt.Println(x)
}
fmt.Println(x)
}
复制代码
在运行代码之前,猜一猜会打印出什么:
- 什么都不打印,代码无法编译
- 第一行10,第二行5,第三行5
- 第一行10,第二行5,第三行10
实际输出是:
10
5
10
复制代码
遮蔽的变量与外层代码块变量名称相同。只要遮蔽变量存在,就无法访问到被遮蔽的变量。
本例中,我们肯定不希望在if语句中新建一个变量x。而是希望将5赋值给函数顶部声明的x。if语句中的第一个fmt.Println,我们可以访问到函数顶部所声明的x。但在接下来的一行中,我们在if语句体代码块内新声明了一个x,这产生了遮蔽。第二个fmt.Println访问变量x时,得到的是遮蔽的变量,值为5。if语句的闭合花括号结束了遮蔽x的代码块,所以在第三个fmt.Println中访问变量x时,得到的又是函数顶部所声明的变量,值为10。注意这个x并未消失也没有被重赋值,只是在内部代码块中遮蔽后无法访问到它而已。
上一章中我们提到有些场景下应避免使用:=,因为那样会不清楚使用的是哪个变量。背后的原因就是使用:=会很容易不小心形成遮蔽。别忘了,我们可以使用:=一次性创建并对多个变量赋值。:=同时左侧的变量不需要全部是新变量。只要:=左侧有一个变量是新变量就合法。我们再来看另一个示例程序(例4-2),读者也在The Go Playground中运行。
例4-2 多赋值语句产生的遮蔽
func main() {
x := 10
if x > 5 {
x, y := 5, 20
fmt.Println(x, y)
}
fmt.Println(x)
}
复制代码
运行代码得到如下结果:
5 20
10
复制代码
虽然外部代码块定义了x,还是会在if语句内被遮蔽。原因在于:=只会重用当前代码块中声明的变量。在使用:=时,确保在左侧不使用外部作用域的变量,除非你就是想进行遮蔽。
还应注意不要遮蔽了导入的包。我们会在模块、包和导入一章中讨论包的导入,但实际上我们已经导入过fmt包用于打印程序的输出。参见例4-3的main函数中声明名为fmt的变量后的效果。读者可以在The Go Playground中运行。
例4-3 包名的遮蔽
func main() {
x := 10
fmt.Println(x)
fmt := "oops"
fmt.Println(fmt)
}
复制代码
运行以上代码会报如下错误:
fmt.Println undefined (type string has no field or method Println)
复制代码
注意问题不在于我们将变量命名为fmt,而在于尝试访问的方法本地变量fmt并没有。在声明了本地变量fmt后,就会遮蔽文件代码块中的fmt包,在main中就无法再使用fmt包了。
检测遮蔽的变量
既然遮蔽会产生不易察觉的bug,那么最好是能保障在程序中不存在遮蔽的变量。go vet或golangci-lint中都没有检测遮蔽的工具,但可以通过在本机上安装shadowlinter工具来在构建过程中完成检测:
$ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
复制代码
如果使用Makefile进行构建,可在vet任务下添加shadow:
vet:
go vet ./...
shadow ./...
.PHONY:vet
复制代码
在对前述代码运行make vet时,会检测到所遮蔽的变量:
declaration of "x" shadows declaration at line 6
复制代码
全局代码块
其实还有一个比较奇怪的代码块:全局代码块。Go是一个只有25个关键字的小型语言。有意思的是其内置类型(如int和string)、常量(如true和false)和函数(如make和close)并没有位列其中。nil也一样,那它们是放在什么位置呢?
Go没有将它们看成关键字,而是看成预定义标识符,在全局代码块中定义,也就是包含所有其它代码块的代码块。
因为这些名称在全局代码块中定义,也就意味着可以在其它作用域中被遮蔽。可以在The Go Playground中查看例4-4的代码。
例4-4 遮蔽true
fmt.Println(true)
true := 10
fmt.Println(true)
复制代码
运行得到的结果如下:
true
10
复制代码
必须要当心不要重新定义全局代码块中标识符。如果不小心这么干了,会产生非常奇怪的效果。幸运的话会报编译错误。但如果没有,会很难在源代码中追踪到问题所在。
你可能会觉得这么具有毁灭性的问题lint工具一定可以捕获到。但事实是不能。即使是用shadow也无法监测到全局代码块标识符的遮蔽。
if
Go语言中的if语句和其它大部分编程语言中的if并没有什么差别。因为我们太熟悉它了,在前面的示例代码中我们已经用过了。例4-5为一个更完整的示例。
例4-5 if和else
n := rand.Intn(10)
if n == 0 {
fmt.Println("That's too low")
} else if n > 5 {
fmt.Println("That's too big:", n)
} else {
fmt.Println("That's a good number:", n)
}
复制代码
注:如果运行这段代码,会发现对n的赋值一直都是1。这是因为math/rand中的随机数种子被硬编码了。在模块、包和导入的重载包名一节中,我们会在演示如何处理包冲突时学习一种生成随机数种子的方式。
Go中与其它编程语言if语句最明显的差别在于条件不放在括号中。并且Go为if语句添加了一项功能可更好地管理变量。
在讨论变量遮蔽时提到过,if或else语句代码块中声明的变量仅存在于该代码块。这并不奇怪,大部分编程

本文详细介绍了Go语言中的代码块、变量遮蔽、if语句、for循环的四种形式(包括for-range)以及switch语句的使用。强调了在Go中避免遮蔽变量的重要性,展示了for-range在遍历字符串、数组、切片和字典时的特性,以及如何使用标签来控制break和continue的行为。此外,还探讨了goto语句在特定情况下的使用。

1497

被折叠的 条评论
为什么被折叠?



