mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-09-09 21:32:09 +00:00
回到简体
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
## 2.7. 作用域
|
||||
|
||||
一個聲明語句將程序中的實體和一個名字關聯,比如一個函數或一個變量。聲明語句的作用域是指源代碼中可以有效使用這個名字的范圍。
|
||||
一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。
|
||||
|
||||
不要將作用域和生命週期混爲一談。聲明語句的作用域對應的是一個源代碼的文本區域;它是一個編譯時的屬性。一個變量的生命週期是指程序運行時變量存在的有效時間段,在此時間區域內它可以被程序的其他部分引用;是一個運行時的概念。
|
||||
不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
|
||||
|
||||
語法塊是由花括弧所包含的一繫列語句,就像函數體或循環體花括弧對應的語法塊那樣。語法塊內部聲明的名字是無法被外部語法塊訪問的。語法決定了內部聲明的名字的作用域范圍。我們可以這樣理解,語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼,我們稱之爲語法塊。有一個語法塊爲整個源代碼,稱爲全局語法塊;然後是每個包的包語法決;每個for、if和switch語句的語法決;每個switch或select的分支也有獨立的語法決;當然也包括顯式書寫的語法塊(花括弧包含的語句)。
|
||||
语法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧对应的语法块那样。语法块内部声明的名字是无法被外部语法块访问的。语法决定了内部声明的名字的作用域范围。我们可以这样理解,语法块可以包含其他类似组批量声明等没有用花括弧包含的代码,我们称之为语法块。有一个语法块为整个源代码,称为全局语法块;然后是每个包的包语法决;每个for、if和switch语句的语法决;每个switch或select的分支也有独立的语法决;当然也包括显式书写的语法块(花括弧包含的语句)。
|
||||
|
||||
聲明語句對應的詞法域決定了作用域范圍的大小。對於內置的類型、函數和常量,比如int、len和true等是在全局作用域的,因此可以在整個程序中直接使用。任何在在函數外部(也就是包級語法域)聲明的名字可以在同一個包的任何源文件中訪問的。對於導入的包,例如tempconv導入的fmt包,則是對應源文件級的作用域,因此隻能在當前的文件中訪問導入的fmt包,當前包的其它源文件無法訪問在當前源文件導入的包。還有許多聲明語句,比如tempconv.CToF函數中的變量c,則是局部作用域的,它隻能在函數內部(甚至隻能是局部的某些部分)訪問。
|
||||
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。任何在在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如tempconv.CToF函数中的变量c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。
|
||||
|
||||
控製流標號,就是break、continue或goto語句後面跟着的那種標號,則是函數級的作用域。
|
||||
控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。
|
||||
|
||||
一個程序可能包含多個同名的聲明,隻要它們在不同的詞法域就沒有關繫。例如,你可以聲明一個局部變量,和包級的變量同名。或者是像2.3.3節的例子那樣,你可以將一個函數參數的名字聲明爲new,雖然內置的new是全局作用域的。但是物極必反,如果濫用不同詞法域可重名的特性的話,可能導致程序很難閲讀。
|
||||
一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者是像2.3.3节的例子那样,你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。
|
||||
|
||||
當編譯器遇到一個名字引用時,如果它看起來像一個聲明,它首先從最內層的詞法域向全局的作用域査找。如果査找失敗,則報告“未聲明的名字”這樣的錯誤。如果該名字在內部和外部的塊分别聲明過,則內部塊的聲明首先被找到。在這種情況下,內部聲明屏蔽了外部同名的聲明,讓外部的聲明的名字無法被訪問:
|
||||
当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:
|
||||
|
||||
```Go
|
||||
func f() {}
|
||||
@@ -27,7 +27,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在函數中詞法域可以深度嵌套,因此內部的一個聲明可能屏蔽外部的聲明。還有許多語法塊是if或for等控製流語句構造的。下面的代碼有三個不同的變量x,因爲它們是定義在不同的詞法域(這個例子隻是爲了演示作用域規則,但不是好的編程風格)。
|
||||
在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -42,11 +42,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在`x[i]`和`x + 'A' - 'a'`聲明語句的初始化的表達式中都引用了外部作用域聲明的x變量,稍後我們會解釋這個。(註意,後面的表達式與unicode.ToUpper併不等價。)
|
||||
在`x[i]`和`x + 'A' - 'a'`声明语句的初始化的表达式中都引用了外部作用域声明的x变量,稍后我们会解释这个。(注意,后面的表达式与unicode.ToUpper并不等价。)
|
||||
|
||||
正如上面例子所示,併不是所有的詞法域都顯式地對應到由花括弧包含的語句;還有一些隱含的規則。上面的for語句創建了兩個詞法域:花括弧包含的是顯式的部分是for的循環體部分詞法域,另外一個隱式的部分則是循環的初始化部分,比如用於迭代變量i的初始化。隱式的詞法域部分的作用域還包含條件測試部分和循環後的迭代部分(`i++`),當然也包含循環體詞法域。
|
||||
正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句创建了两个词法域:花括弧包含的是显式的部分是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分,比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分(`i++`),当然也包含循环体词法域。
|
||||
|
||||
下面的例子同樣有三個不同的x變量,每個聲明在不同的詞法域,一個在函數體詞法域,一個在for隱式的初始化詞法域,一個在for循環體詞法域;隻有兩個塊是顯式創建的:
|
||||
下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
和for循環類似,if和switch語句也會在條件部分創建隱式詞法域,還有它們對應的執行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍:
|
||||
和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:
|
||||
|
||||
```Go
|
||||
if x := f(); x == 0 {
|
||||
@@ -71,11 +71,11 @@ if x := f(); x == 0 {
|
||||
fmt.Println(x, y) // compile error: x and y are not visible here
|
||||
```
|
||||
|
||||
第二個if語句嵌套在第一個內部,因此第一個if語句條件初始化詞法域聲明的變量在第二個if中也可以訪問。switch語句的每個分支也有類似的詞法域規則:條件部分爲一個隱式詞法域,然後每個是每個分支的詞法域。
|
||||
第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后每个是每个分支的词法域。
|
||||
|
||||
在包級别,聲明的順序併不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用後面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數。但是如果一個變量或常量遞歸引用了自身,則會産生編譯錯誤。
|
||||
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。
|
||||
|
||||
在這個程序中:
|
||||
在这个程序中:
|
||||
|
||||
```Go
|
||||
if f, err := os.Open(fname); err != nil { // compile error: unused: f
|
||||
@@ -85,9 +85,9 @@ f.ReadByte() // compile error: undefined f
|
||||
f.Close() // compile error: undefined f
|
||||
```
|
||||
|
||||
變量f的作用域隻有在if語句內,因此後面的語句將無法引入它,這將導致編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示,具體錯誤信息依賴編譯器的實現。
|
||||
变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。
|
||||
|
||||
通常需要在if之前聲明變量,這樣可以確保後面的語句依然可以訪問變量:
|
||||
通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(fname)
|
||||
@@ -98,7 +98,7 @@ f.ReadByte()
|
||||
f.Close()
|
||||
```
|
||||
|
||||
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
|
||||
你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:
|
||||
|
||||
```Go
|
||||
if f, err := os.Open(fname); err != nil {
|
||||
@@ -110,9 +110,9 @@ if f, err := os.Open(fname); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
但這不是Go語言推薦的做法,Go語言的習慣是在if中處理錯誤然後直接返迴,這樣可以確保正常執行的語句不需要代碼縮進。
|
||||
但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。
|
||||
|
||||
要特别註意短變量聲明語句的作用域范圍,考慮下面的程序,它的目的是獲取當前的工作目録然後保存到一個包級的變量中。這可以本來通過直接調用os.Getwd完成,但是將這個從主邏輯中分離出來可能會更好,特别是在需要處理錯誤的時候。函數log.Fatalf用於打印日誌信息,然後調用os.Exit(1)終止程序。
|
||||
要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这可以本来通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -125,9 +125,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
雖然cwd在外部已經聲明過,但是`:=`語句還是將cwd和err重新聲明爲新的局部變量。因爲內部聲明的cwd將屏蔽外部的聲明,因此上面的代碼併不會正確更新包級聲明的cwd變量。
|
||||
虽然cwd在外部已经声明过,但是`:=`语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。
|
||||
|
||||
由於當前的編譯器會檢測到局部聲明的cwd併沒有本使用,然後報告這可能是一個錯誤,但是這種檢測併不可靠。因爲一些小的代碼變更,例如增加一個局部cwd的打印語句,就可能導致這種檢測失效。
|
||||
由于当前的编译器会检测到局部声明的cwd并没有本使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -141,9 +141,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
全局的cwd變量依然是沒有被正確初始化的,而且看似正常的日誌輸出更是讓這個BUG更加隱晦。
|
||||
全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。
|
||||
|
||||
有許多方式可以避免出現類似潛在的問題。最直接的方法是通過單獨聲明err變量,來避免使用`:=`的簡短聲明方式:
|
||||
有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用`:=`的简短声明方式:
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@@ -157,5 +157,5 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
我們已經看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節,我們將探討數據的結構。
|
||||
我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。
|
||||
|
||||
|
Reference in New Issue
Block a user