本文將解釋代碼塊和標(biāo)識(shí)符的作用域。
(注意:本文中描述的代碼塊的層級關(guān)系和Go白皮書中有所不同。)
Go代碼中有四種代碼塊。
{}
?中的代碼形成了一個(gè)局部代碼塊。 但是也有一些局部代碼塊并不包含在一對大括號(hào)中,這樣的代碼塊稱為隱式代碼塊,而包含在一對大括號(hào)中的局部代碼塊稱為顯式代碼塊。 組合字面量中的大括號(hào)和代碼塊無關(guān)。各種控制流程中的一些關(guān)鍵字跟隨著一些隱式局部代碼塊:
if
、switch
或者for
關(guān)鍵字跟隨著兩個(gè)內(nèi)嵌在一起的局部代碼塊。 其中一個(gè)代碼塊是隱式的,另一個(gè)是顯式的,此顯式的代碼塊內(nèi)嵌在此隱式代碼塊之中。 如果這樣的一個(gè)關(guān)鍵字跟隨著一個(gè)變量短聲明形式,則被聲明的變量聲明在此隱式代碼塊中。else
關(guān)鍵字可以跟隨著一個(gè)顯式或者隱式代碼塊。此顯式或者隱式代碼塊內(nèi)嵌在跟隨在對應(yīng)if
關(guān)鍵字后的隱式代碼塊中。 如果此else
關(guān)鍵字立即跟隨著另一個(gè)if
關(guān)鍵字,則跟隨在此else
關(guān)鍵字后的代碼塊可以為隱式的,否則,此代碼塊必須為顯式的。select
關(guān)鍵字跟隨著一個(gè)顯式局部代碼塊。case
和default
關(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)系:
go/*
標(biāo)準(zhǔn)庫認(rèn)為文件代碼塊內(nèi)嵌在包代碼塊中。)go/*
標(biāo)準(zhǔn)庫認(rèn)為頂層局部代碼塊內(nèi)嵌在文件代碼中。) (本文和go/*
標(biāo)準(zhǔn)庫的解釋有所不同的原因是為了讓下面對標(biāo)識(shí)符遮擋的解釋更加簡單和清楚。)
下面是一張展示上述代碼塊層級關(guān)系的圖片:
代碼塊主要用來解釋各種代碼元素聲明中的標(biāo)識(shí)符的可聲明位置和作用域。
在一個(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)體字段變量聲明。
所以,
請注意:
if
、switch
或者for
關(guān)鍵字后可以緊跟著一條變量短聲明語句;
select
控制流程中的每個(gè)case
關(guān)鍵字后可以緊跟著一條變量短聲明語句。
(順便說一下,go/*
標(biāo)準(zhǔn)庫代碼包認(rèn)為文件代碼塊中只能包含包引入聲明。)
聲明在包代碼塊中并且在所有局部代碼塊之外的代碼元素稱為包級(package-level)元素。 包級元素可以是具名常量、變量、函數(shù)、定義類型或類型別名。
一個(gè)代碼元素標(biāo)識(shí)符的作用域是指此標(biāo)識(shí)符可被識(shí)別的代碼范圍(或可見范圍)。
不考慮本文最后一節(jié)將要解釋的標(biāo)識(shí)符遮擋,Go白皮書這樣描述各種代碼元素的標(biāo)識(shí)符的作用域:
空標(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í)符的作用域差異的例子:
不考慮跳轉(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
、cap
、nil
等,并不是關(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è)有用的特性,但是千萬不要濫用之。
更多建議: