代码块和标识符作用域

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

(注意:本文中描述的代码块的层级关系和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: 不可循环引用
		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中很多常见的名称,比如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 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感兴趣。

The English version of this book is here.
赞赏
(《Go语言101》系列丛书由老貘从2016年7月开始编写。目前此系列丛书仍在不断改进和增容中。你的赞赏是本系列丛书和此Go101.org网站不断增容和维护的动力。)

目录: