W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
原文鏈接:https://gopl-zh.github.io/ch2/ch2-07.html
一個聲明語句將程序中的實體和一個名字關聯(lián),比如一個函數(shù)或一個變量。聲明語句的作用域是指源代碼中可以有效使用這個名字的范圍。
不要將作用域和生命周期混為一談。聲明語句的作用域?qū)氖且粋€源代碼的文本區(qū)域;它是一個編譯時的屬性。一個變量的生命周期是指程序運行時變量存在的有效時間段,在此時間區(qū)域內(nèi)它可以被程序的其他部分引用;是一個運行時的概念。
句法塊是由花括弧所包含的一系列語句,就像函數(shù)體或循環(huán)體花括弧包裹的內(nèi)容一樣。句法塊內(nèi)部聲明的名字是無法被外部塊訪問的。這個塊決定了內(nèi)部聲明的名字的作用域范圍。我們可以把塊(block)的概念推廣到包括其他聲明的群組,這些聲明在代碼中并未顯式地使用花括號包裹起來,我們稱之為詞法塊。對全局的源代碼來說,存在一個整體的詞法塊,稱為全局詞法塊;對于每個包;每個for、if和switch語句,也都有對應詞法塊;每個switch或select的分支也有獨立的詞法塊;當然也包括顯式書寫的詞法塊(花括弧包含的語句)。
聲明語句對應的詞法域決定了作用域范圍的大小。對于內(nèi)置的類型、函數(shù)和常量,比如int、len和true等是在全局作用域的,因此可以在整個程序中直接使用。任何在函數(shù)外部(也就是包級語法域)聲明的名字可以在同一個包的任何源文件中訪問的。對于導入的包,例如tempconv導入的fmt包,則是對應源文件級的作用域,因此只能在當前的文件中訪問導入的fmt包,當前包的其它源文件無法訪問在當前源文件導入的包。還有許多聲明語句,比如tempconv.CToF函數(shù)中的變量c,則是局部作用域的,它只能在函數(shù)內(nèi)部(甚至只能是局部的某些部分)訪問。
控制流標號,就是break、continue或goto語句后面跟著的那種標號,則是函數(shù)級的作用域。
一個程序可能包含多個同名的聲明,只要它們在不同的詞法域就沒有關系。例如,你可以聲明一個局部變量,和包級的變量同名。或者是像2.3.3節(jié)的例子那樣,你可以將一個函數(shù)參數(shù)的名字聲明為new,雖然內(nèi)置的new是全局作用域的。但是物極必反,如果濫用不同詞法域可重名的特性的話,可能導致程序很難閱讀。
當編譯器遇到一個名字引用時,它會對其定義進行查找,查找過程從最內(nèi)層的詞法域向全局的作用域進行。如果查找失敗,則報告“未聲明的名字”這樣的錯誤。如果該名字在內(nèi)部和外部的塊分別聲明過,則內(nèi)部塊的聲明首先被找到。在這種情況下,內(nèi)部聲明屏蔽了外部同名的聲明,讓外部的聲明的名字無法被訪問:
func f() {}
var g = "g"
func main() {
f := "f"
fmt.Println(f) // "f"; local var f shadows package-level func f
fmt.Println(g) // "g"; package-level var
fmt.Println(h) // compile error: undefined: h
}
在函數(shù)中詞法域可以深度嵌套,因此內(nèi)部的一個聲明可能屏蔽外部的聲明。還有許多語法塊是if或for等控制流語句構造的。下面的代碼有三個不同的變量x,因為它們是定義在不同的詞法域(這個例子只是為了演示作用域規(guī)則,但不是好的編程風格)。
func main() {
x := "hello!"
for i := 0; i < len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
}
在x[i]
和x + 'A' - 'a'
聲明語句的初始化的表達式中都引用了外部作用域聲明的x變量,稍后我們會解釋這個。(注意,后面的表達式與unicode.ToUpper并不等價。)
正如上面例子所示,并不是所有的詞法域都顯式地對應到由花括弧包含的語句;還有一些隱含的規(guī)則。上面的for語句創(chuàng)建了兩個詞法域:花括弧包含的是顯式的部分,是for的循環(huán)體部分詞法域,另外一個隱式的部分則是循環(huán)的初始化部分,比如用于迭代變量i的初始化。隱式的詞法域部分的作用域還包含條件測試部分和循環(huán)后的迭代部分(i++
),當然也包含循環(huán)體詞法域。
下面的例子同樣有三個不同的x變量,每個聲明在不同的詞法域,一個在函數(shù)體詞法域,一個在for隱式的初始化詞法域,一個在for循環(huán)體詞法域;只有兩個塊是顯式創(chuàng)建的:
func main() {
x := "hello"
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
和for循環(huán)類似,if和switch語句也會在條件部分創(chuàng)建隱式詞法域,還有它們對應的執(zhí)行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍:
if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
第二個if語句嵌套在第一個內(nèi)部,因此第一個if語句條件初始化詞法域聲明的變量在第二個if中也可以訪問。switch語句的每個分支也有類似的詞法域規(guī)則:條件部分為一個隱式詞法域,然后是每個分支的詞法域。
在包級別,聲明的順序并不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用后面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數(shù)。但是如果一個變量或常量遞歸引用了自身,則會產(chǎn)生編譯錯誤。
在這個程序中:
if f, err := os.Open(fname); err != nil { // compile error: unused: f
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
變量f的作用域只在if語句內(nèi),因此后面的語句將無法引入它,這將導致編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示,具體錯誤信息依賴編譯器的實現(xiàn)。
通常需要在if之前聲明變量,這樣可以確保后面的語句依然可以訪問變量:
f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
if f, err := os.Open(fname); err != nil {
return err
} else {
// f and err are visible here too
f.ReadByte()
f.Close()
}
但這不是Go語言推薦的做法,Go語言的習慣是在if中處理錯誤然后直接返回,這樣可以確保正常執(zhí)行的語句不需要代碼縮進。
要特別注意短變量聲明語句的作用域范圍,考慮下面的程序,它的目的是獲取當前的工作目錄然后保存到一個包級的變量中。這本來可以通過直接調(diào)用os.Getwd完成,但是將這個從主邏輯中分離出來可能會更好,特別是在需要處理錯誤的時候。函數(shù)log.Fatalf用于打印日志信息,然后調(diào)用os.Exit(1)終止程序。
var cwd string
func init() {
cwd, err := os.Getwd() // compile error: unused: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
雖然cwd在外部已經(jīng)聲明過,但是:=
語句還是將cwd和err重新聲明為新的局部變量。因為內(nèi)部聲明的cwd將屏蔽外部的聲明,因此上面的代碼并不會正確更新包級聲明的cwd變量。
由于當前的編譯器會檢測到局部聲明的cwd并沒有使用,然后報告這可能是一個錯誤,但是這種檢測并不可靠。因為一些小的代碼變更,例如增加一個局部cwd的打印語句,就可能導致這種檢測失效。
var cwd string
func init() {
cwd, err := os.Getwd() // NOTE: wrong!
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}
全局的cwd變量依然是沒有被正確初始化的,而且看似正常的日志輸出更是讓這個BUG更加隱晦。
有許多方式可以避免出現(xiàn)類似潛在的問題。最直接的方法是通過單獨聲明err變量,來避免使用:=
的簡短聲明方式:
var cwd string
func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
我們已經(jīng)看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節(jié),我們將探討數(shù)據(jù)的結構。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: