Go 语言中的作用域

MoodWu · 2018-06-20 22:37:01 · 2427 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-06-20 22:37:01 的文章,其中的信息可能已经有所发展或是发生改变。

理解 Go 语言中的作用域是怎么起作用的,需要一些关于块的预备知识,这在 “Go 语言中的代码块” 文章中有讲。

一个标识符的作用域是标识符与某个值,比如变量、常量、包等,进行绑定的那一部分源码(有时甚至是全部)。

package main
import "fmt"
func main() {
    {
        v := 1
        {
            fmt.Println(v)
        }
        fmt.Println(v)
    }
    // 编译错误:“undefined: v”
    // fmt.Println(v)
}

对于有经验的工程师,很容易就能判断出程序的输出应该是这样的:

> ./bin/sandbox
1
1

最后一行的 fmt.Println 被注释了,因为它会引起编译错误。很快我们就来解释这是为什么。简单的说,变量 v 在包含定义它代码块的大括号之外,就超出它的作用域了。

值得一提的是,给变量赋一个新的值并不影响它的作用域(也叫可见性):

v := 1
{
    v = 2  // 赋值
    fmt.Println(v)
}
fmt.Println(v)

输出:

>./bin/sandbox
2
2

而且它与下面的代码运行结果不同:

v := 1
{
    v := 2  // 简短变量声明方式
    fmt.Println(v)
}
fmt.Println(v)

这段代码的输出是:

>./bin/sandbox
2
1

作用域与标识符的定义紧密相关(更准确的说是与标识符被声明的地方)

变量或者常量的声明

变量标识符的作用域能到达最内层的代码块(不论是隐含的或者是显式用大括号包围起来的):

func main() {
    {
        v := 1
        {
            fmt.Println(v)
            {
                fmt.Println(v)
            }
        }
        fmt.Println(v)
    }
}

这段代码是 100% 有效的代码,运行结果为:

> ./bin/sandbox
1
1
1

作用域从变量被声明的那一行代码开始。

func main() {
    fmt.Println(v)
    v := 1
}

所以这段代码会抛出一个编译错误 “undefined: v”。简短变量声明方式可以一次性声明多个变量:

a, b := 0, 1

但是标识符从它被声明语句结束的地方开始有效,所以,下面这一句是错的:

a, b := 1, a  // 未定义的: a

对于简短变量声明,上述作用域规则同样适用:

  • 变量声明(使用 var 关键字)
  • 常量声明(使用 const 声明)

在括起来的变量或常量声明中,变量或者常量从它们被声明的语句之后就生效,而不需要等整个括起来的代码结束,所以下面的代码是有效的:

var (
    a = 1  // 变量声明 no. 1
    b = a  // 变量声明 no. 2
)
fmt.Println(a, b)

运行结果是:

> ./bin/sandbox
1 1

同样的,如果在括起来的声明中,如果用简短方式声明多个变量,

var (
    a, b = 1, a
)

这样的代码同样会报编译错误 —— “undefined: a”

类型声明

就作用域而言,类型声明与变量或者常量一样 —— 一直作用到最内层的代码块。但是与变量或常量不同的是,类型声明从标识结束的地方就开始生效了,而不是从类型定义代码结束的地方才生效。这一点额外的“空间”让类型递归称为可能:

type X struct {
    name string
    next *X
}
x := X{name: "foo", next: &X{name: "bar"}}
fmt.Println(x.name)
fmt.Println(x.next.name)
fmt.Println(x.next.next)

输出:

> ./bin/sandbox
foo
bar
<nil>

next 字段必须是一个指针,下面这样的定义是不合法的:

type X struct {
    name string
    next X
}

因为编译器会抛出 “invalid recursive type X” 的错误,产生这个错误的原因是,当创建类型 X 时,要计算这个类型的大小,而编译器发现类型 X 的 next 字段也是 X 类型,一个同样的还没有确定大小的字段,于是我们会陷入一个无穷递归中。但是如果是一个指针类,编译器就能知道在指定平台上指针类型的确定大小。

预定义标识符

有很多内置的标识符:

  • 类型:bool, int32, int64, float64, …
  • nil
  • 函数: make, new, panic, …
  • 常量,比如 true/false

它们有全局的作用域,所以它们可以在代码的任何地方使用。

Imports

当导入包时,包内名称的作用域就是在文件块内。这样包内的标识符只能在包已经被正确导入后,通过 f.ex 的方式来引用。

// sandbox.go
package main
import "fmt"
func main() {
    fmt.Println("main")
    f()
}
// utils.go
package main
func f() {
    fmt.Println("f")
}

当编译以上包时,编译器会抛出错误:“undefined: fmt in fmt.Println”。

顶级的标识符

在任何函数外声明的变量,常量,类型,函数,在整个包内是可见的(作用域是整个包)

// sandbox.go
package main
func main() {
    f()
}
// utils.go
package main
import "fmt"
func f() {
    fmt.Println("It works!")
}

以上代码可以编译并运行输出:

> ./bin/sandbox
It works!

函数和方法

方法的调用者,函数参数 或者 返回值仅仅在函数体内可以访问 —— 这个显而易见的就不用代码演示了。

遮蔽(Shadowing)

在同一个代码块中,一个标识符不能被声明两次。但是在内部的代码块中可以重新声明外部被声明了的标识符(代码块可以像洋葱那样一层层嵌套的)。如果在内层重新声明了标识符,那么在代码中起作用的声明是离代码最近的最里层的那个声明:

v := "outer"
fmt.Println(v)
{
    v := "inner"
    fmt.Println(v)
    {
        fmt.Println(v)
    }
}
{
    fmt.Println(v)
}
fmt.Println(v)

输出:

> ./bin/sandbox
outer
inner
inner
outer
outer

参考资料


via: https://medium.com/golangspec/scopes-in-go-a6042bb4298c

作者:Michał Łowicki  译者:MoodWu  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2427 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
1 回复  |  直到 2018-06-21 11:57:49
xuanjiang1985
xuanjiang1985 · #1 · 7年之前

手动点👍

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传