[Go]程序结构——作用域

来源:互联网 发布:merge算法 c语言 编辑:程序博客网 时间:2024/06/05 04:40

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中刻意有效使用这个名字的范围。

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

语法块是由花括号所包含的一些列语句,就像函数体或循环体那样。块内部声明的对象是无法被外部成员访问的。有一个语法块可以包含整个源代码中声明的对象,称为全局语法块;然后是每个包的包语法块;再然后是函数内部的语法块;再然后是for、if和switch语句的语法块;最后是这些语句内部的由花括号显示指定的语法块。

整个源代码>包>文件>函数>循环/分支>循环/分支内部显示花括号

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如 int、len和true等是全局作用域,因此可以在整个程序中直接使用。任何在函数外部(包级)声明的名字可以在同一个包的任何源文件中访问。对于导入的包,例如tempconv导入的fmt包,则是对应文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问此源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域,它只能在函数内部(甚至是函数内部的某些部分)访问。

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

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

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

func f() {}var g = "g"func main() {    f := "f"    fmt.Println(f) // "f"; local var f shadows package-level func f    fmt.Println(g) // "g"; package-level var    fmt.Println(h) // compile error: undefined: h}

在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。下面的代码有三个不同的变量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" (one letter per iteration)        }    }}
在 x[i ]和 x + 'A' - 'a'声明语句的初始化表达式中,都引用了外部作用域声明的变量x,稍后我们会解释这个。

正如上面例子所示,并不是所有的词法域都显示地对应到由花括号包含的语句,还有一些隐式的规则。上面的for语句创建了两个词法域:花括号包含的for循环体是显示部分,

另一个隐式部分则是循环的初始化部分,比如用于迭代变量的初始化。隐式的词法域部分还包含条件测试语和i++。

下面的例子同样有三个不同的变量x,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个是显示创建的。

func main() {    x := "hello"    for _, x := range x {        x := x + 'A' - 'a'        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)    }}
和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) // compile error: x and y are not visible here
第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分是一个隐式词法域,然后是每个分支的词法域。

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

if f, err := os.Open(fname); err != nil { // compile error: unused: f    return err}f.ReadByte() // compile error: undefined ff.Close()    // compile error: undefined f
变量f的作用域只有在if语句内,因此后面的语句将无法引用它,这将导致编译错误。

通常需要在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 and err are visible here too    f.ReadByte()    f.Close()}
但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

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

var cwd stringfunc init() {    cwd, err := os.Getwd() // compile error: unused: cwd    if err != nil {        log.Fatalf("os.Getwd failed: %v", err)    }}
虽然cwd在外部已经生命果,但是:=语句还是将cwd重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级别的变量cwd。由于当前的编译器会检测到局部变量cwd并没有使用,所以会编译失败。但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。

var cwd stringfunc 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 stringfunc init() {    var err error    cwd, err = os.Getwd()    if err != nil {        log.Fatalf("os.Getwd failed: %v", err)    }}