本文将解释代码块和标识符的作用域。
(注意:本文中描述的代码块的层级关系和Go白皮书中有所不同。)
{}
中的代码形成了一个局部代码块。
但是也有一些局部代码块并不包含在一对大括号中,这样的代码块称为隐式代码块,而包含在一对大括号中的局部代码块称为显式代码块。
组合字面量中的大括号和代码块无关。
if
、switch
或者for
关键字跟随着两个内嵌在一起的局部代码块。
其中一个代码块是隐式的,另一个是显式的,此显式的代码块内嵌在此隐式代码块之中。
如果这样的一个关键字跟随着一个变量短声明形式,则被声明的变量声明在此隐式代码块中。
else
关键字可以跟随着一个显式或者隐式代码块。此显式或者隐式代码块内嵌在跟随在对应if
关键字后的隐式代码块中。
如果此else
关键字立即跟随着另一个if
关键字,则跟随在此else
关键字后的代码块可以为隐式的,否则,此代码块必须为显式的。
select
关键字跟随着一个显式局部代码块。
case
和default
关键字后跟随着一个隐式代码块,此隐式代码块内嵌在对应的switch
或者select
关键字后跟随的显式代码块中。
不内嵌在任何其它局部代码块中的局部代码块称为顶层(或者包级)局部代码块。顶层局部代码块肯定都是函数体。
注意,一个函数声明中的输入参数和输出结果变量都被看作是声明在此函数体代码块内,虽然看上去它们好像声明在函数体代码块之外。
go/*
标准库认为文件代码块内嵌在包代码块中。)go/*
标准库认为顶层局部代码块内嵌在文件代码中。)
(本文和go/*
标准库的解释有所不同的原因是为了让下面对标识符遮挡的解释更加简单和清楚。)
代码块主要用来解释各种代码元素声明中的标识符的可声明位置和作用域。
在一个代码元素的声明中,一个标识符和一个代码元素绑定在了一起。 或者说,在此声明中,被声明的代码元素将被赋予此标识符做为它的名称。 此后,我们就可以用此标识符来代表此代码元素。
下标展示了各种代码元素可以被直接声明在何种代码块中:万物代码块 | 包代码块 | 文件代码块 | 局部代码块 | |
---|---|---|---|---|
预声明的(即内置的)代码元素(1) | 可以 | |||
包引入 | 可以 | |||
定义类型和类型别名(不含内置的) | 可以 | 可以 | 可以 | |
具名常量(不含内置常量) | 可以 | 可以 | 可以 | |
变量(不含内置变量)(2) | 可以 | 可以 | 可以 | |
函数(不含内置函数) | 可以 | 可以 | ||
跳转标签 | 可以 |
(1) 预声明代码元素展示在builtin
标准库中。
(2) 不包括结构体字段变量声明。
if
、switch
或者for
关键字后可以紧跟着一条变量短声明语句;
select
控制流程中的每个case
关键字后可以紧跟着一条变量短声明语句。
(顺便说一下,go/*
标准库代码包认为文件代码块中只能包含包引入声明。)
声明在包代码块中并且在所有局部代码块之外的代码元素称为包级(package-level)元素。 包级元素可以是具名常量、变量、函数、定义类型或类型别名。
一个代码元素标识符的作用域是指此标识符可被识别的代码范围(或可见范围)。
不考虑本文最后一节将要解释的标识符遮挡,Go白皮书这样描述各种代码元素的标识符的作用域:空标识符没有作用域。
(注意,预声明的iota
标识符只能使用在常量声明中。)
package main
func main() {
// var v int = v // error: v未定义
// const C int = C // error: C未定义
/*
type T = struct {
*T // error: 不可循环引用
x []T // error: 不可循环引用
}
*/
// 下面所有的类型定义声明都是合法的。
type T struct {
*T
x []T
}
type A [5]*A
type S []S
type M map[int]M
type F func(F) F
type Ch chan Ch
type P *P
// ...
var p P
p = &p
p = ***********************p
***********************p = p
var s = make(S, 3)
s[0] = s
s = s[0][0][0][0][0][0][0][0]
var m = M{}
m[1] = m
m = m[1][1][1][1][1][1][1][1]
}
注意:fmt.Print(s)
和fmt.Print(m)
调用都将导致恐慌(因为堆栈溢出)。
package main
// 下面这两行中各自等号左边和右边的标识符表示同一个代码元素。
// 右边的标识符不是预声明的标识符。
/*
const iota = iota // error: 循环引用
var true = true // error: 循环引用
*/
var a = b // 可以使用其后声明的变量的标识符
var b = 123
func main() {
// 下面两行中右边的标识符为预声明的标识符。
const iota = iota // ok
var true = true // ok
_ = true
// 下面几行编译不通过。
/*
var c = d // 不能使用其后声明变量标识符
var d = 123
_ = c
*/
}
不考虑跳转标签,一个在外层代码块直接声明的标识符将被在内层代码块直接声明的相同标识符所遮挡。
跳转标签标识符不会被遮挡。
如果一个标识符被遮挡了,它的作用域将不包括遮挡它的标识符的作用域。
下面是一个有趣的例子。在此例子中,有6个变量均被声明为x
。
一个在更深层代码块中声明的x
遮挡了所有在外层声明的x
。
package main
import "fmt"
var p0, p1, p2, p3, p4, p5 *int
var x = 9999 // x#0
func main() {
p0 = &x
var x = 888 // x#1
p1 = &x
for x := 70; x < 77; x++ { // x#2
p2 = &x
x := x - 70 // // x#3
p3 = &x
if x := x - 3; x > 0 { // x#4
p4 = &x
x := -x // x#5
p5 = &x
}
}
// 9999 888 77 6 3 -3
fmt.Println(*p0, *p1, *p2, *p3, *p4, *p5)
}
下面是另一个关于标识符遮挡和作用域的例子。此例子程序运行将输出
Sheep Goat
而不是Sheep Sheep
。
请阅读其中的注释获取原因。
package main
import "fmt"
var f = func(b bool) {
fmt.Print("Goat")
}
func main() {
var f = func(b bool) {
fmt.Print("Sheep")
if b {
fmt.Print(" ")
f(!b) // 此f乃包级变量f也。
}
}
f(true) // 此f为刚声明的局部变量f。
}
如果我们将上例更改为如下所示,则此程序将运行输出
Sheep Sheep
。
func main() {
var f func(b bool)
f = func(b bool) {
fmt.Print("Sheep")
if b {
fmt.Print(" ")
f(!b) // 现在,此f变为局部变量f了。
}
}
f(true)
}
在某些情况下,当一些标识符被内层的一个变量短声明中声明的变量所遮挡时,一些新手Go程序员会搞不清楚此变量短声明中声明的哪些变量是新声明的变量。 下面这个例子(含有bug)展示了Go编程中一个比较有名的陷阱。 几乎每个Go程序员在刚开始使用Go的时候都曾经掉入过此陷阱。
package main
import "fmt"
import "strconv"
func parseInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
// 一些新手Go程序员会认为下一行中声明
// 的err变量已经在外层声明过了。然而其
// 实下一行中的b和err都是新声明的变量。
// 此新声明的err遮挡了外层声明的err。
b, err := strconv.ParseBool(s)
if err != nil {
return 0, err
}
// 如果代码运行到这里,一些新手Go程序员
// 期望着内层的nil err将被返回。但是其实
// 返回是外层的非nil err。因为内层的err
// 的作用域到外层if代码块结尾就结束了。
if b {
n = 1
}
}
return n, err
}
func main() {
fmt.Println(parseInt("TRUE"))
}
程序输出:
1 strconv.Atoi: parsing "TRUE": invalid syntax
Go语言目前只有25个关键字。 关键字不能被用做标识符。Go中很多常见的名称,比如
int
、bool
、string
、len
、cap
、nil
等,并不是关键字,它们是预声明标识符。
这些预声明的标识符声明在万物代码块中,所以它们可以被声明在内层的相同标识符所遮挡。
下面是一个展示了预声明标识符被遮挡的古怪的例子。它编译和运行都没有问题。
package main
import (
"fmt"
)
const len = 3 // 遮挡了内置函数len
var true = 0 // 遮挡了内置常量true
type nil struct {} // 遮挡了内置变量nil
func int(){} // 遮挡了内置类型int
func main() {
fmt.Println("a weird program")
var output = fmt.Println
var fmt = [len]nil{{}, {}, {}} // 遮挡了包引入fmt
// var n = len(fmt) // error: len是一个常量
var n = cap(fmt) // 我们只好使用内置cap函数
// for关键字跟随着一个隐式代码块和一个显式代码块。
// 变量短声明中的true遮挡了全局变量true。
for true := 0; true < n; true++ {
// 下面声明的false遮挡了内置常量false。
var false = fmt[true]
// 下面声明的true遮挡了循环变量true。
var true = true+1
// 下一行编译不通过,因为fmt是一个数组。
// fmt.Println(true, false)
output(true, false)
}
}
输出结果:
a weird program
1 {}
2 {}
3 {}
是的,此例子是一个极端的例子。标识符遮挡是一个有用的特性,但是千万不要滥用之。
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .
来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync
标准库包sync/atomic
标准库包