网站地图    收藏    合作   
<

快捷菜单 返回顶部

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。在今后的学习中我们将会学习到像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。

对全局的源代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个 for、if 和 switch 语句,也都对应词法块;每个 switch 或 select 的分支也有独立的语法块;当然也包括显式书写的词法块(花括弧包含的语句)。

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如 int、len 和 true 等是在全局作用域的,因此可以在整个程序中直接使用。

任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如 tempconv 导入的 fmt 包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的 fmt 包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如 tempconv.CToF 函数中的变量 c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。

控制流标号,就是 break、continue 或 goto 语句后面跟着的那种标号,则是函数级的作用域。

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者可以将一个函数参数的名字声明为 new,虽然内置的 new 是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。

当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:
func f() {}

var g = "g"

func main() {
    f := "f"
    fmt.Println(f) // "f"; 局部变量 f shadows 包级别函数 f
    fmt.Println(g) // "g"; 包级别变量
    fmt.Println(h) // 编译错误: undefined: h
}
在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是 if 或 for 等控制流语句构造的。下面的代码有三个不同的变量 x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。
func main() {
    x := "hello!"
    for i := 0; i < len(x); i++ {
        x := x[i]
        if x != '!' {
            x := x + 'A' - 'a'
            fmt.Printf("%c", x) // "HELLO" (每次迭代一个字母)
        }
    }
}
在 x[i] 和 x + 'A' - 'a' 声明语句的初始化的表达式中都引用了外部作用域声明的 x 变量。(注意,后面的表达式与 unicode.ToUpper 并不等价。)

正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的 for 语句创建了两个词法域:花括弧包含的是显式的部分是 for 的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量 i 的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(i++),当然也包含循环体词法域。

下面的例子同样有三个不同的 x 变量,每个声明在不同的词法域,一个在函数体词法域,一个在 for 隐式的初始化词法域,一个在 for 循环体词法域;只有两个块是显式创建的:
func main() {
    x := "hello"
    for _, x := range x {
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (每次迭代一个字母)
    }
}
和 for 循环类似,if 和 switch 语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的 if-else 测试链演示了 x 和 y 的有效作用域范围:
if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // 编译错误: x 和 y 未定义
第二个 if 语句嵌套在第一个内部,因此第一个 if 语句条件初始化词法域声明的变量在第二个 if 中也可以访问。switch 语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后每个是每个分支的词法域。

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

在下面这个程序中:
if f, err := os.Open(fname); err != nil { // 编译错误: unused: f
    return err
}
f.ReadByte() // 编译错误: undefined f
f.Close()    // 编译错误: undefined f
变量 f 的作用域只有在 if 语句内,因此后面的语句将无法引入它,这将导致编译错误。可能会收到一个局部变量 f 没有声明的错误提示,具体错误信息依赖编译器的实现。

通常需要在 if 之前声明变量,这样可以确保后面的语句依然可以访问变量:
f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()
可能会考虑通过将 ReadByte 和 Close 移动到 if 的 else 块来解决这个问题:
if f, err := os.Open(fname); err != nil {
    return err
} else {
    // 这里可以看到 f 和 err
    f.ReadByte()
    f.Close()
}
但这不是 Go语言推荐的做法,Go语言的习惯是在 if 中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这可以本来通过直接调用 os.Getwd 完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数 log.Fatalf 用于打印日志信息,然后调用 os.Exit(1) 终止程序。
var cwd string

func init() {
    cwd, err := os.Getwd() // 编译错误: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}
虽然 cwd 在外部已经声明过,但是 := 语句还是将 cwd 和 err 重新声明为新的局部变量。因为内部声明的 cwd 将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的 cwd 变量。

由于当前的编译器会检测到局部声明的 cwd 并没有本使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部 cwd 的打印语句,就可能导致这种检测失效。
var cwd string

func init() {
    cwd, err := os.Getwd() // NOTE: wrong!
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}
全局的 cwd 变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个 BUG 更加隐晦。

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明 err 变量,来避免使用 := 的简短声明方式:
var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com