代码块和标识符作用域

本文将解释代码块和标识符的作用域。

(注意:本文中描述的代码块的层级关系和Go白皮书中有所不同。)

代码块

Go代码中有四种代码块。 各种控制流程中的一些关键字跟随着一些隐式局部代码块:

不内嵌在任何其它局部代码块中的局部代码块称为顶层(或者包级)局部代码块。顶层局部代码块肯定都是函数体。

注意,一个函数声明中的输入参数和输出结果变量都被看作是声明在此函数体代码块内,虽然看上去它们好像声明在函数体代码块之外。

各种代码块的层级关系:

(本文和Go白皮书及go/*标准库的解释有所不同的原因是为了让下面对标识符遮挡的解释更加简单和清楚。)

下面是一张展示上述代码块层级关系的图片:
代码块层级关系

代码块主要用来解释各种代码元素声明中的标识符的可声明位置和作用域。

各种代码元素的可声明位置

我们可以声明六种代码元素:

在一个代码元素的声明中,一个标识符和一个代码元素绑定在了一起。 或者说,在此声明中,被声明的代码元素将被赋予此标识符做为它的名称。 此后,我们就可以用此标识符来代表此代码元素。

下标展示了各种代码元素可以被直接声明在何种代码块中:
万物代码块 包代码块 文件代码块 局部代码块
预声明的(即内置的)代码元素(1) 可以
包引入 可以
定义类型和类型别名(不含内置的) 可以 可以 可以
有名常量(不含内置常量) 可以 可以 可以
变量(不含内置变量)(2) 可以 可以 可以
函数(不含内置函数) 可以 可以
跳转标签 可以

(1) 预声明代码元素声明在builtin标准库中。
(2) 不包括结构体字段变量声明。

所以, 请注意:

(顺便说一下,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: 不可循环引用
	}
	*/

	// 下面所有的类型定义声明都是合法的。
	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 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]

	var p P
	p = &p
	p = ***********************p
}

下面是一个展示了包级声明和局部声明的标识符的作用域差异的例子:
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)
}

在某些情况下,当一些标识符被内层的一个变量短声明中声明的变量所遮挡时,一些新手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中很多常见的名称,比如intboolstringlencapnil等,并不是关键字,它们是预声明标识符。 这些预声明的标识符声明在万物代码块中,所以它们可以被声明在内层的相同标识符所遮挡。 下面是一个展示了预声明标识符被遮挡的古怪的例子。它编译和运行都没有问题。
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 print = 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)
		print(true, false)
	}
}
输出结果:
a weird program
1 {}
2 {}
3 {}

是的,此例子是一个极端的例子。标识符遮挡是一个有用的特性,但是千万不要滥用之。

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏