声明、类型、语句与控制结构
18 理解 Go 语言代码块与作用域
Go 语言中的代码块(code block)是包裹在一对大括号内部的声明和语句,且代码块支持嵌套。如果一对大括号之间没有任何语句,那么称这个代码块为空代码块。代码块是代码执行流流转的基本单元,代码执行流总是从一个代码块跳到另一个代码块。
Go 语言中有两类代码块:
- 一类是我们在代码中直观可见的由一堆大括号包裹的显式代码块,比如函数的函数体、for 循环的循环体、if 语句的某个分支等。
- 另一类是没有大括号包裹的隐式代码块。Go 规范定义了如下几种:
- 宇宙(Universe)代码块:所有 Go 源码都在该隐式代码块中,相当于所有 Go 代码的最外层都存在一对大括号。
- 包代码块:每个包都有一个包代码块,其中放置着该包的所有 Go 源码。
- 文件代码块:每个文件都有一个文件代码块,其中包含着该文件中的所有 Go 源码。
- 每个 if、for 和 switch 语句均被视为位于其自己的隐式代码块中。
- switch 或 select 语句中的每个子句都被视为一个隐式代码块。
Go 标识符的作用域(scope)是基于代码块定义的,作用域规则描述了标识符在哪些代码块中是有效的。
作用域规则:
- 预定义标识符,make、new、cap、len 等的作用域范围是宇宙块。
- 顶层(任何函数之外),声明的常量、类型、变量或函数(但不是方法)对应的标识符的作用域范围是包代码块。比如:包级变量、包级常量的标识符的作用域都是包代码块。
- Go 源文件中导人的包名称的作用域范围是文件代码块。
- 方法接收器(receiver)、函数参数或返回值变量对应的标识符的作用域范围是函数体(显式代码块),虽然它们并没有被函数体的大括号所显式包裹。
- 在函数内部声明的常量或变量对应的标识符的作用域范围始于常量或变量声明语句的末尾,止于其最里面的那个包含块的末尾。
- 在函数内部声明的类型标识符的作用城范围始于类型定义中的标识符,止于其最里面的那个包含块的末尾。
if 条件控制语句中的代码块:
// if {} else if {} else {} 型
if Stmt1; Expr1 {
...
} else if Stmt2; Expr2{
...
} else {
...
}
// 等价变换
{ // 隐式代码块 1 开始
Stmt1
if Expr1 { // 显式代码块 1 开始
...
} else{ // 显式代码块 1 结束;显式代码块 2 开始
{ // 隐式代码块 2 开始
Stmt2
if Expr2{ // 显式代码块 3 开始
...
} else { // 显式代码块 3 结束;显式代码块 4 开始
...
} // 显式代码块 4 结束
} // 隐式代码块 2 结束
} // 显式代码块 2 结束
} // 隐式代码块 1 结束
其他控制语句(for、switch、select)的代码块规则可以类比上述 if 语句的代码块规则。要注意的是,和 switch-case 无法在 case 子句中声明变量不同的是,select-case 可以在 case 子句中通过短变量声明定义新变量,但该变量依然被纳入 case 的隐式代码块中。
c1 := make(chan int)
c2 := make(chan int, 1)
c2 <- 11
select {
case c1 <- 1:
fmt.Println("send")
case i := <-c2:
_ = i
fmt.Println("recv")
default:
fmt.Println("default")
}
// 等价变换
select {
case c1 <- 1:
{
fmt.Println("send")
}
case "如果该 case 被选择": // 伪代码
{
i := <-c2
_ = i
fmt.Println("recv")
}
default:
{
fmt.Println("default")
}
}
// 执行结果:recv
往期回顾
- 「读书笔记」声明、零值可用、复合字面值
- 「读书笔记」了解切片实现原理并高效使用
- 「读书笔记」了解 map 实现原理并高效使用
- 「读书笔记」了解 string 实现原理并高效使用
- 「读书笔记」理解 Go 语言的包导入
- 「读书笔记」理解 Go 语言表达式的求值顺序
关注我
参考
《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明