前言
如果不深入理解 Go 语言的代码块作用域,程序将产生我们无法理解的行为,比如说在循环中创建 goroutine func, 为什么需要传递参数至 goroutine 内部,否则所有的 func 使用的变量参数都是循环的最后一个值。
看下边这个 demo, 就需要深入理解 Go 语言代码块的作用域才能理直气壮的一口答对:
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
println(a, b, c)
}
}
有两个答案选项:
A:1 2 3
B:无法通过编译
思考三秒钟🤔-----------------
正确答案是 A,你答对了吗,接下来将围绕上述例子来理解一下Go代码块(code block)和作用域(scope)规则,理解这些规则将有助于我们编写出正确且可读性高的代码。
代码块与作用域简介
Go语言中的代码块是包裹在一对大括号内部的声明和语句,且代码块支持嵌套。如果一对大括号之间没有任何语句,那么称这个代码块为空代码块。代码块是代码执行流流转的基本单元,代码执行流总是从一个代码块跳到另一个代码块。
显示代码块
- 由大括号包裹,比如函数体、for 循环的循环体、if 语句的某个分支等。
隐式代码块
- 宇宙代码块: 所有Go源码都在该隐式代码块中,就相当于所有Go代码的最外层都存在一对大括号。
- 包代码块: 每个包都有一个包代码块,其中放置着该包的所有Go源码。
- 文件代码块:每个文件都有一个文件代码块,其中包含着该文件中的所有Go源码。
- 每个if、for和switch语句均被视为位于其自己的隐式代码块中。
- switch或select语句中的每个子句都被视为一个隐式代码块。
Go标识符的作用域是基于代码块定义的,作用域规则描述了标识符在哪些代码块中是有效的。下面是标识符作用域规则。
在函数内部声明的类型标识符的作用域范围始于类型定义中的标识符,止于其最里面的那个包含块的末尾:
if 条件控制语句的代码块
func Foo() {
if a := 1; true {
fmt.Println(a)
}
}
// 等价变换为
func Foo() {
{
a := 1
if true {
fmt.Println(a)
}
}
}
func Foo() {
if a,b := 1, 2; false {
fmt.Println(a)
} else {
fmt.Println(b)
}
}
// 等价变换为
func Foo() {
{
a, b := 1, 2
if false {
fmt.Println(a)
} else {
fmt.Println(b)
}
}
}
func main() {
if a := 1; false {
} else if b := 2; false {
} else if c := 3; false {
} else {
println(a, b, c)
}
}
// 进行等价变换后
func main() {
{
a := 1
if false {
} else {
{
b := 2
if false {
} else {
{
c := 3
if false {
} else {
println(a, b, c)
}
}
}
}
}
}
}
其他控制语句的代码块
for a, b := 1, 10; a < b; a++ {
...
}
// 等价转换为
{
a, b := 1, 10
for ; a < b; a++ {
...
}
}
var sl = []int{1, 2, 3}
for i, n := range sl {
...
}
// 等价变换为
var sl = []int{1, 2, 3}
{
i, n := 0, 0
for i, n := range sl {
...
}
}
switch x, y := 1, 2; x + y {
case 3:
a := 1
fmt.Println("case1: a = ", a)
fallthrough
case 10:
a := 5
fmt.Println("case2: a = ", a)
fallthrough
default:
a := 7
fmt.Println("default case: a = ", a)
}
// 等价变换为
{
x, y := 1, 2
switch x + y {
case 3:
{
a := 1
fmt.Println("case1: a = ", a)
}
fallthrough
case 10:
{
a := 5
fmt.Println("case2: a = ", a)
}
fallthrough
default:
{
a := 7
fmt.Println("default case: a = ", a)
}
}
}
c1 := make(chan int)
c2 := make(chan int, 1)
c2 <- 11
select {
case c1 <- 1:
fmt.Println("SendStmt case has been chosen")
case i := <-c2:
_ = i
fmt.Println("RecvStmt case has been chosen")
default:
fmt.Println("default case has been chosen")
}
// 等价变换为 (伪代码)
c1 := make(chan int)
c2 := make(chan int, 1)
c2 <- 11
select {
case c1 <- 1:
{
fmt.Println("SendStmt case has been chosen")
}
case "如果该case 被选择":
{
i := <-c2:
_ = i
fmt.Println("RecvStmt case has been chosen")
}
default:
{
fmt.Println("default case has been chosen")
}
}
总结
各类隐式代码块的规则才是理解Go代码块和作用域的规则的“金钥匙”,尤其是那些对于程序执行流有重大影响的控制语句的隐式代码块规则。
理解Go代码块和作用域的规则将有助于我们快速解决类似“变量未定义”的错误和上一层变量被内层同名变量遮蔽(shadow)的问题,同时对于正确理解Go程序的执行流也大有裨益。