Go語言 代碼塊和標(biāo)識(shí)符作用域

2023-02-16 17:39 更新

本文將解釋代碼塊和標(biāo)識(shí)符的作用域。

(注意:本文中描述的代碼塊的層級關(guān)系和Go白皮書中有所不同。)

代碼塊

Go代碼中有四種代碼塊。

  • 萬物代碼塊(the universe code block)。一個(gè)程序只有一個(gè)萬物代碼塊,它包含著一個(gè)程序中所有的代碼;
  • 包代碼塊(package code block)。一個(gè)包代碼塊含著一個(gè)代碼包中的所有代碼,但不包括此代碼包中的源代碼文件中的所有引入聲明。
  • 文件代碼塊(file code block)。一個(gè)文件代碼塊包含著一個(gè)源文件中的所有代碼,包括此文件中的包引入聲明。
  • 局部代碼塊(local code block)。一般說來,一對大括號(hào)?{}?中的代碼形成了一個(gè)局部代碼塊。 但是也有一些局部代碼塊并不包含在一對大括號(hào)中,這樣的代碼塊稱為隱式代碼塊,而包含在一對大括號(hào)中的局部代碼塊稱為顯式代碼塊。 組合字面量中的大括號(hào)和代碼塊無關(guān)。

各種控制流程中的一些關(guān)鍵字跟隨著一些隱式局部代碼塊:

  • 一個(gè)if、switch或者for關(guān)鍵字跟隨著兩個(gè)內(nèi)嵌在一起的局部代碼塊。 其中一個(gè)代碼塊是隱式的,另一個(gè)是顯式的,此顯式的代碼塊內(nèi)嵌在此隱式代碼塊之中。 如果這樣的一個(gè)關(guān)鍵字跟隨著一個(gè)變量短聲明形式,則被聲明的變量聲明在此隱式代碼塊中。
  • 一個(gè)else關(guān)鍵字可以跟隨著一個(gè)顯式或者隱式代碼塊。此顯式或者隱式代碼塊內(nèi)嵌在跟隨在對應(yīng)if關(guān)鍵字后的隱式代碼塊中。 如果此else關(guān)鍵字立即跟隨著另一個(gè)if關(guān)鍵字,則跟隨在此else關(guān)鍵字后的代碼塊可以為隱式的,否則,此代碼塊必須為顯式的。
  • 一個(gè)select關(guān)鍵字跟隨著一個(gè)顯式局部代碼塊。
  • 每個(gè)casedefault關(guān)鍵字后跟隨著一個(gè)隱式代碼塊,此隱式代碼塊內(nèi)嵌在對應(yīng)的switch或者select關(guān)鍵字后跟隨的顯式代碼塊中。

不內(nèi)嵌在任何其它局部代碼塊中的局部代碼塊稱為頂層(或者包級)局部代碼塊。頂層局部代碼塊肯定都是函數(shù)體。

注意,一個(gè)函數(shù)聲明中的輸入?yún)?shù)和輸出結(jié)果變量都被看作是聲明在此函數(shù)體代碼塊內(nèi),雖然看上去它們好像聲明在函數(shù)體代碼塊之外。

各種代碼塊的層級關(guān)系:

  • 所有的包代碼塊均直接內(nèi)嵌在萬物代碼塊中;
  • 所有的文件代碼塊也均直接內(nèi)嵌在萬物代碼塊中;(注意:go/*標(biāo)準(zhǔn)庫認(rèn)為文件代碼塊內(nèi)嵌在包代碼塊中。)
  • 每個(gè)頂層局部代碼塊同時(shí)直接內(nèi)嵌在一個(gè)包代碼塊和一個(gè)文件代碼塊中;(注意:go/*標(biāo)準(zhǔn)庫認(rèn)為頂層局部代碼塊內(nèi)嵌在文件代碼中。)
  • 一個(gè)非頂層局部代碼塊肯定直接內(nèi)嵌在另一個(gè)局部代碼塊中。

(本文和go/*標(biāo)準(zhǔn)庫的解釋有所不同的原因是為了讓下面對標(biāo)識(shí)符遮擋的解釋更加簡單和清楚。)

下面是一張展示上述代碼塊層級關(guān)系的圖片:


代碼塊主要用來解釋各種代碼元素聲明中的標(biāo)識(shí)符的可聲明位置和作用域。

各種代碼元素的可聲明位置

我們可以聲明六種代碼元素:

  • 包引入;
  • 定義類型和類型別名;
  • 具名常量;
  • 變量;
  • 函數(shù);
  • 跳轉(zhuǎn)標(biāo)簽。

在一個(gè)代碼元素的聲明中,一個(gè)標(biāo)識(shí)符和一個(gè)代碼元素綁定在了一起。 或者說,在此聲明中,被聲明的代碼元素將被賦予此標(biāo)識(shí)符做為它的名稱。 此后,我們就可以用此標(biāo)識(shí)符來代表此代碼元素。

下標(biāo)展示了各種代碼元素可以被直接聲明在何種代碼塊中:

萬物代碼塊 包代碼塊 文件代碼塊 局部代碼塊
預(yù)聲明的(即內(nèi)置的)代碼元素(1) 可以
包引入 可以
定義類型和類型別名(不含內(nèi)置的) 可以 可以 可以
具名常量(不含內(nèi)置常量) 可以 可以 可以
變量(不含內(nèi)置變量)(2) 可以 可以 可以
函數(shù)(不含內(nèi)置函數(shù)) 可以 可以
跳轉(zhuǎn)標(biāo)簽 可以

(1) 預(yù)聲明代碼元素展示在builtin標(biāo)準(zhǔn)庫中。

(2) 不包括結(jié)構(gòu)體字段變量聲明。

所以,

  • 包引入不能聲明在包代碼塊和局部代碼塊中;
  • 函數(shù)不能被聲明在局部代碼塊中;(匿名函數(shù)可以定義在局部代碼塊中,但它們不屬于元素聲明。)
  • 跳轉(zhuǎn)標(biāo)簽只能被聲明在局部代碼塊中。

請注意:

  • 如果包含兩個(gè)代碼元素聲明的最內(nèi)層代碼塊為同一個(gè),則這兩個(gè)代碼元素不能同名。
  • 聲明在一個(gè)包中的一個(gè)包級代碼元素的名稱不能和此包中任何源文件中的包引入名同名。 或者反過來說更容易理解:一個(gè)包中的任何包引入名不能和此包中的任何包級代碼元素的名稱重名。 此規(guī)則可能會(huì)在以后被放寬。
  • 如果包含兩個(gè)跳轉(zhuǎn)標(biāo)簽的最內(nèi)層函數(shù)體為同一個(gè),則這兩個(gè)標(biāo)簽不能同名;
  • 一個(gè)跳轉(zhuǎn)標(biāo)簽的所有引用必須處于包含此跳轉(zhuǎn)標(biāo)簽聲明的最內(nèi)層函數(shù)體代碼塊內(nèi);
  • 各種控制流程中的隱式代碼塊對元素聲明有特殊的要求。 一般說來,聲明語句不允許出現(xiàn)在這樣的隱式代碼塊中,除了一些變量短聲明:
    • 每個(gè)if、switch或者for關(guān)鍵字后可以緊跟著一條變量短聲明語句;
    • 一個(gè)select控制流程中的每個(gè)case關(guān)鍵字后可以緊跟著一條變量短聲明語句。

(順便說一下,go/*標(biāo)準(zhǔn)庫代碼包認(rèn)為文件代碼塊中只能包含包引入聲明。)

聲明在包代碼塊中并且在所有局部代碼塊之外的代碼元素稱為包級(package-level)元素。 包級元素可以是具名常量、變量、函數(shù)、定義類型或類型別名。

代碼元素標(biāo)識(shí)符的作用域

一個(gè)代碼元素標(biāo)識(shí)符的作用域是指此標(biāo)識(shí)符可被識(shí)別的代碼范圍(或可見范圍)。

不考慮本文最后一節(jié)將要解釋的標(biāo)識(shí)符遮擋,Go白皮書這樣描述各種代碼元素的標(biāo)識(shí)符的作用域:

  • 內(nèi)置代碼元素標(biāo)識(shí)符的作用域?yàn)檎麄€(gè)萬物代碼塊;
  • 一個(gè)包引入聲明標(biāo)識(shí)符的作用域?yàn)榘穆暶鞯奈募a塊;
  • 直接聲明在一個(gè)包代碼塊中的一個(gè)常量、類型、變量或者函數(shù)(不包括方法)的標(biāo)識(shí)符的作用域?yàn)榇税a塊;
  • 在函數(shù)體中聲明的一個(gè)常量或者變量的標(biāo)識(shí)符的作用域起始于此常量或者變量的描述(specification)的結(jié)尾(對于變量短聲明,為此變量聲明的結(jié)尾),并終止于包含此常量或者變量的聲明的最內(nèi)層代碼塊的結(jié)尾;
  • 一個(gè)函數(shù)參數(shù)(包括方法屬主參數(shù))和結(jié)果標(biāo)識(shí)符的作用域?yàn)槠鋵?yīng)函數(shù)體局部代碼塊;
  • 在函數(shù)體中聲明的一個(gè)類型的標(biāo)識(shí)符的作用域起始于此類型的描述中它的標(biāo)識(shí)符的結(jié)尾,并終止于包含此類型的聲明的最內(nèi)層代碼塊的結(jié)尾;
  • 一個(gè)跳轉(zhuǎn)標(biāo)簽的標(biāo)識(shí)符的作用域?yàn)榘藰?biāo)簽的聲明的最內(nèi)層函數(shù)體代碼塊,但要排除掉此內(nèi)嵌在此最內(nèi)層函數(shù)體代碼塊中的各個(gè)匿名函數(shù)體代碼塊。
  • 關(guān)于類型參數(shù)的作用域,請閱讀《Go自定義泛型101》一書。

空標(biāo)識(shí)符沒有作用域。

(注意,預(yù)聲明的iota標(biāo)識(shí)符只能使用在常量聲明中。)

你可能已經(jīng)注意到了局部定義類型的作用域和其它局部元素(變量、常量和類型別名)的作用域的定義有微小的差別。 此差別體現(xiàn)在一個(gè)定義類型的聲明中可以立即使用此定義類型的標(biāo)識(shí)符。 下面這個(gè)例子展示了這一差異:

package main

func main() {
	// var v int = v   // error: v未定義
	// const C int = C // error: C未定義
	/*
	type T = struct {
		*T    // error: 不可循環(huán)引用
		x []T // error: 不可循環(huán)引用
	}
	*/

	// 下面所有的類型定義聲明都是合法的。
	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)調(diào)用都將導(dǎo)致恐慌(因?yàn)槎褩R绯觯?br>

下面是一個(gè)展示了包級聲明和局部聲明的標(biāo)識(shí)符的作用域差異的例子:

標(biāo)識(shí)符遮擋

不考慮跳轉(zhuǎn)標(biāo)簽,一個(gè)在外層代碼塊直接聲明的標(biāo)識(shí)符將被在內(nèi)層代碼塊直接聲明的相同標(biāo)識(shí)符所遮擋。

跳轉(zhuǎn)標(biāo)簽標(biāo)識(shí)符不會(huì)被遮擋。

如果一個(gè)標(biāo)識(shí)符被遮擋了,它的作用域?qū)⒉话ㄕ趽跛臉?biāo)識(shí)符的作用域。

下面是一個(gè)有趣的例子。在此例子中,有6個(gè)變量均被聲明為x。 一個(gè)在更深層代碼塊中聲明的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)
}

下面是另一個(gè)關(guān)于標(biāo)識(shí)符遮擋和作用域的例子。此例子程序運(yùn)行將輸出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。
}

如果我們將上例更改為如下所示,則此程序?qū)⑦\(yùn)行輸出Sheep Sheep。

func main() {
	var f func(b bool)
	f = func(b bool) {
		fmt.Print("Sheep")
		if b {
			fmt.Print(" ")
			f(!b) // 現(xiàn)在,此f變?yōu)榫植孔兞縡了。
		}
	}
	f(true)
}

在某些情況下,當(dāng)一些標(biāo)識(shí)符被內(nèi)層的一個(gè)變量短聲明中聲明的變量所遮擋時(shí),一些新手Go程序員會(huì)搞不清楚此變量短聲明中聲明的哪些變量是新聲明的變量。 下面這個(gè)例子(含有bug)展示了Go編程中一個(gè)比較有名的陷阱。 幾乎每個(gè)Go程序員在剛開始使用Go的時(shí)候都曾經(jīng)掉入過此陷阱。

package main

import "fmt"
import "strconv"

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		// 一些新手Go程序員會(huì)認(rèn)為下一行中聲明
		// 的err變量已經(jīng)在外層聲明過了。然而其
		// 實(shí)下一行中的b和err都是新聲明的變量。
		// 此新聲明的err遮擋了外層聲明的err。
		b, err := strconv.ParseBool(s)
		if err != nil {
			return 0, err
		}

		// 如果代碼運(yùn)行到這里,一些新手Go程序員
		// 期望著內(nèi)層的nil err將被返回。但是其實(shí)
		// 返回是外層的非nil err。因?yàn)閮?nèi)層的err
		// 的作用域到外層if代碼塊結(jié)尾就結(jié)束了。
		if b {
			n = 1
		}
	}
	return n, err
}

func main() {
	fmt.Println(parseInt("TRUE"))
}

程序輸出:

1 strconv.Atoi: parsing "TRUE": invalid syntax

Go語言目前只有25個(gè)關(guān)鍵字。 關(guān)鍵字不能被用做標(biāo)識(shí)符。Go中很多常見的名稱,比如int、bool、string、len、capnil等,并不是關(guān)鍵字,它們是預(yù)聲明標(biāo)識(shí)符。 這些預(yù)聲明的標(biāo)識(shí)符聲明在萬物代碼塊中,所以它們可以被聲明在內(nèi)層的相同標(biāo)識(shí)符所遮擋。 下面是一個(gè)展示了預(yù)聲明標(biāo)識(shí)符被遮擋的古怪的例子。它編譯和運(yùn)行都沒有問題。

package main

import (
	"fmt"
)

const len = 3      // 遮擋了內(nèi)置函數(shù)len
var true = 0       // 遮擋了內(nèi)置常量true
type nil struct {} // 遮擋了內(nèi)置變量nil
func int(){}       // 遮擋了內(nèi)置類型int

func main() {
	fmt.Println("a weird program")
	var output = fmt.Println

	var fmt = [len]nil{{}, {}, {}} // 遮擋了包引入fmt
	// var n = len(fmt) // error: len是一個(gè)常量
	var n = cap(fmt)    // 我們只好使用內(nèi)置cap函數(shù)

	// for關(guān)鍵字跟隨著一個(gè)隱式代碼塊和一個(gè)顯式代碼塊。
	// 變量短聲明中的true遮擋了全局變量true。
	for true := 0; true < n; true++ {
		// 下面聲明的false遮擋了內(nèi)置常量false。
		var false = fmt[true]
		// 下面聲明的true遮擋了循環(huán)變量true。
		var true = true+1
		// 下一行編譯不通過,因?yàn)閒mt是一個(gè)數(shù)組。
		// fmt.Println(true, false)
		output(true, false)
	}
}

輸出結(jié)果:

a weird program
1 {}
2 {}
3 {}

是的,此例子是一個(gè)極端的例子。標(biāo)識(shí)符遮擋是一個(gè)有用的特性,但是千萬不要濫用之。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)