18. 理解Go 语言中的 语句块与作用域

3,995 阅读6分钟

Hi,大家好,我是明哥。

在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。

我的在线博客:golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime


由于 Go 使用的是词法作用域,而词法作用域依赖于语句块。所以在讲作用域时,需要先了解一下 Go 中的语句块是怎么一回事?

1. 显示语句块与隐式语句块

通俗地说,语句块是由花括弧({})所包含的一系列语句。

语句块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围,也就是作用域。

用花括弧包含的语句块,属于显示语句块。

在 Go 中还有很多的隐式语句块:

  • 主语句块:包括所有源码,对应内置作用域
  • 包语句块:包括该包中所有的源码(一个包可能会包括一个目录下的多个文件),对应包级作用域
  • 文件语句块:包括该文件中的所有源码,对应文件级作用域
  • for 、if、switch等语句本身也在它自身的隐式语句块中,对应局部作用域

前面三点好理解,第四点举几个例子

for 循环完后,不能再使用变量 i

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

if 语句判断完后,同样不能再使用变量 i

if i := 0; i >= 0 {
    fmt.Println(i)
}

switch 语句完了后,也是不是再使用变量 i

switch i := 2; i * 4 {
case 8:
    fmt.Println(i)
default:
    fmt.Println(“default”)
}

且每个 switch 语句的子句都是一个隐式的语句块

switch i := 2; i * 4 {
case 8:
    j := 0
    fmt.Println(i, j)
default:
    // "j" is undefined here
    fmt.Println(“default”)
}
// "j" is undefined here

2. 四种作用域的理解

变量的声明,除了声明其类型,其声明的位置也有讲究,不同的位置决定了其拥有不同的作用范围,说白了就是我这个变量,在哪里可用,在哪里不可用。

根据声明位置的不同,作用域可以分为以下四个类型:

  • 内置作用域:不需要自己声明,所有的关键字和内置类型、函数都拥有全局作用域
  • 包级作用域:必須函数外声明,在该包内的所有文件都可以访问
  • 文件级作用域:不需要声明,导入即可。一个文件中通过import导入的包名,只在该文件内可用
  • 局部作用域:在自己的语句块内声明,包括函数,for、if 等语句块,或自定义的 {} 语句块形成的作用域,只在自己的局部作用域内可用

以上的四种作用域,从上往下,范围从大到小,为了表述方便,我这里自己将范围大的作用域称为高层作用域,而范围小的称为低层作用域。

对于作用域,有以下几点总结:

  • 低层作用域,可以访问高层作用域
  • 同一层级的作用域,是相互隔离的
  • 低层作用域里声明的变量,会覆盖高层作用域里声明的变量

在这里要注意一下,不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。

而一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

3. 静态作用域与动态作用域

根据局部作用域内变量的可见性,是否是静态不变,可以将编程语言分为如下两种:

  • 静态作用域,如 Go 语言
  • 动态作用域,如 Shell 语言

具体什么是动态作用域,这里用 Shell 的代码演示一下,你就知道了

#!/bin/bash
func01() {
    local value=1
    func02
}
func02() {
    echo "func02 sees value as ${value}"
}

# 执行函数
func01
func02

从代码中,可以看到在 func01 函数中定义了个局部变量 value,按理说,这个 value 变量只在该函数内可用,但由于在 shell 中的作用域是动态的,所以在 func01中也可以调用 func02 时,func02 可以访问到 value 变量,此时的 func02 作用域可以当成是 局部作用域中(func01)的局部作用域。

但若脱离了 func01的执行环境,将其放在全局环境下或者其他函数中, func02 是访问不了 value 变量的。

所以此时的输出结果是

func02 sees value as 1
func02 sees value as 

但在 Go 中并不存在这种动态作用域,比如这段代码,在func01函数中,要想取得 name 这个变量,只能从func01的作用域或者更高层作用域里查找(文件级作用域,包级作用域和内置作用域),而不能从调用它的另一个局部作用域中(因为他们在层级上属于同一级)查找。

import "fmt"

func func01() {
    fmt.Println("在 func01 函数中,name:", name)
}

func main()  {
    var name string = "Python编程时光"
    fmt.Println("在 main 函数中,name:", name)

    func01()
}

因此你在执行这段代码时,会报错,提示在func01中的name还未定义。

参考文章:studygolang.com/articles/12…

系列导读

01. 开发环境的搭建(Goland & VS Code)

02. 学习五种变量创建的方法

03. 详解数据类型:**整形与浮点型**

04. 详解数据类型:byte、rune与string

05. 详解数据类型:数组与切片

06. 详解数据类型:字典与布尔类型

07. 详解数据类型:指针

08. 面向对象编程:结构体与继承

09. 一篇文章理解 Go 里的函数

10. Go语言流程控制:if-else 条件语句

11. Go语言流程控制:switch-case 选择语句

12. Go语言流程控制:for 循环语句

13. Go语言流程控制:goto 无条件跳转

14. Go语言流程控制:defer 延迟调用

15. 面向对象编程:接口与多态

16. 关键字:make 和 new 的区别?

17. 一篇文章理解 Go 里的语句块与作用域

18. 学习 Go 协程:goroutine

19. 学习 Go 协程:详解信道/通道

20. 几个信道死锁经典错误案例详解

21. 学习 Go 协程:WaitGroup

22. 学习 Go 协程:互斥锁和读写锁

23. Go 里的异常处理:panic 和 recover

24. 超详细解读 Go Modules 前世今生及入门使用

25. Go 语言中关于包导入必学的 8 个知识点

26. 如何开源自己写的模块给别人用?

27. 说说 Go 语言中的类型断言?

28. 这五点带你理解Go语言的select用法