Go语言基础之函数作用域

Go语言基础之函数作用域

作用域指的是声明语句的作用域。声明语句如变量的声明、函数的声明等,都是将一个实体绑定给一个名字,而声明语句的作用域指的就是在源代码中可以有效使用该名字的范围

作用域 VS 声明周期

  • 声明语句的作用域对应的是一个源代码的文本区域,是一个编译时的属性。
  • 声明语句创造一个变量,该变量的生命周期指的是在程序运行时,变量存在的有效时间段,在此时间段内该变量是可以被程序的其他部分引用的,所以说声明周期是一个运行时的概念

一、如何区分作用域范围

狭义地讲,我们通常认为用花括号{}包含的一系列语句称之为一个语法块,广义地讲,Go语言中有以下几种语法块

  • 整个源代码就是一个大的代码块->全局语法块
  • 一个包的所有代码=>包语法块
  • 每个for、if和switch语句的语法块
  • 每个switch或select的分支也有独立的语法块;
  • 当然也包括显式书写的语法块(花括弧包含的语句)。

声明语句所在的语法块称之为它的词法域,即名字定义的地方,也就是说,层层嵌套的词法域是以定义阶段为准生成的,而在Go语言中,采用的就是词法作用域,换句话说,定义位置决定了作用域的嵌套关系而与引用位置无关,即无论在何处引用/查找名字,都需要老老老实实地回到该名字当初定义的地方去找作用域关系,由内向外一层层查找

词法域与动态作用域

编译器的第一个工作阶段叫做词法化,也叫单词化,所以说,词法作用域是在词法阶段生成的作用域,是一个编译时的概念。与词法域相对应的是动态作用域,二者对比如下

  • 词法作用域(又称静态作用域)是在书写代码或者说定义时确定的,而动态作用域是在运行时确定的。
  • 词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈的

二、词法作用域

词法作用域,或简称作用域,有如下几种

1 全局作用域

对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。

2 包级作用域

任何在在函数外部(也就是包级词法域)声明的名字是在包级作用域,可以在同一个包的任何源文件中访问

3 源文件级作用域

对于导入的包,例如在当前文件中导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,即便是当前包的其它源文件也无法访问在当前源文件导入的包,若也想使用包fmt,需要重新导入才行

4 局部作用域

函数内部的名字,只能在函数内使用

5 函数内嵌套的作用域

函数内可以再有{},可以是for、if、switch等{},{}内定义的名字只能在{}内使用,

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系,例如,你可以声明一个局部变量,和包级的变量同名。当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问。

但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读,go语言不推荐这么做。

一、全局作用域

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testGlobalVar() {
    fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
    testGlobalVar() //num=10
}

二、局部变量

局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x:

func testLocalVar() {
    //定义一个函数局部变量x,仅在该函数内生效
    var x int64 = 100
    fmt.Printf("x=%d\n", x)
}

func main() {
    testLocalVar()
    fmt.Println(x) // 此时无法使用变量x
}

如果局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testNum() {
    num := 100
    fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
    testNum() // num=100
}

接下来我们来看一下语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。

func testLocalVar2(x, y int) {
    fmt.Println(x, y) //函数的参数也是只在本函数中生效
    if x > 0 {
        z := 100 //变量z只在if语句块生效
        fmt.Println(z)
    }
    //fmt.Println(z)//此处无法使用变量z
}

还有我们之前讲过的for循环语句中定义的变量,也是只在for语句块中生效:

func testLocalVar3() {
    for i := 0; i < 10; i++ {
        fmt.Println(i) //变量i只在当前for语句块中生效
    }
    //fmt.Println(i) //此处无法使用变量i
}

三、函数类型与变量

语句块定义的变量

我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x, y int) int {
    return x + y
}

func sub(x, y int) int {
    return x - y
}

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add

四、函数类型变量

我们可以声明函数类型的变量并且为该变量赋值:

func main() {
    var c calculation               // 声明一个calculation类型的变量c
    c = add                         // 把add赋值给c
    fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
    fmt.Println(c(1, 2))            // 像调用add一样调用c

    f := add                        // 将函数add赋值给变量f1
    fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
    fmt.Println(f(10, 20))          // 像调用add一样调用f
}

五、总结

  1. 全局作用域,全局都可以被访问到
  2. 局部变量,只可以在函数内部访问,局部变量和全局变量同名优先访问局部,语句块定义的变量只能在当前语句块可以访问
  3. 定义函数类型:type calculation func(int, int) int
  4. 函数类型变量,凡是满足这个条件的函数都是calculation类型的函数,都可以被赋值调用
posted @ 2021-10-16 16:00  RandySun  阅读(206)  评论(0编辑  收藏  举报