mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-01 02:28:56 +00:00
163 lines
8.0 KiB
Markdown
163 lines
8.0 KiB
Markdown
## 2.7. 作用域
|
|
|
|
一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的范圍.
|
|
|
|
不要將作用域和生命週期混爲一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概念.
|
|
|
|
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域范圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之爲詞滙塊. 有一個語法決爲整個源代碼, 稱爲全局塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
|
|
|
|
聲明的詞法域決定了作用域范圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全局作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在同一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是局部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
|
|
|
|
控製流標籤, 例如 break, continue 或 goto 後面跟着的那種標籤, 則是函數級的作用域.
|
|
|
|
一個程序可能包含多個同名的聲明, 隻有它們在不同的詞法域就沒有關繫. 例如, 你可以聲明一個局部變量, 和包級的變量同名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明爲 new, 雖然內置的new是全局作用域的. 但是物極必反, 如果濫用重名的特性, 可能導致程序很難閲讀.
|
|
|
|
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域向全局的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分别聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部同名的聲明, 讓外部的聲明無法被訪問:
|
|
|
|
```Go
|
|
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
|
|
}
|
|
```
|
|
|
|
在函數中詞法域可以深度嵌套, 因此內部的一個聲明可能屏蔽外部的聲明. 還有許多塊是if或for等控製流語句構造的. 下面的代碼有三個不同的變量x, 因爲它們是定義在不同的詞法域的原因. (這個例子隻是爲了演示作用域規則, 但不是好的編程風格.)
|
|
|
|
```Go
|
|
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併不等價.)
|
|
|
|
正如上面所示, 併不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上面的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
|
|
|
|
下面的例子同樣有三個不同的x變量, 每個聲明在不同的塊, 一個在函數體塊, 一個在for語句塊, 一個在循環體塊; 隻有兩個塊是顯式創建的:
|
|
|
|
```Go
|
|
func main() {
|
|
x := "hello"
|
|
for _, x := range x {
|
|
x := x + 'A' - 'a'
|
|
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
|
|
}
|
|
}
|
|
```
|
|
|
|
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下面的 if-else 測試鏈演示的 x 和 y 的作用域范圍:
|
|
|
|
```Go
|
|
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語句嵌套在第一個內部, 因此一個if語句條件塊聲明的變量在第二個if中也可以訪問. switch語句的每個分支也有類似的規則: 條件部分爲一個隱式塊, 然後每個是每個分支的主體塊.
|
|
|
|
在包級别, 聲明的順序併不會影響作用域范圍, 因此一個先聲明的可以引用它自身或者是引用後面的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會産生編譯錯誤.
|
|
|
|
在這個程序中:
|
|
|
|
```Go
|
|
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語句內, 因此後面的語句將無法引入它, 將導致編譯錯誤. 你可能會收到一個局部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
|
|
|
|
通常需要在if之前聲明變量, 這樣可以確保後面的語句依然可以訪問變量:
|
|
|
|
```Go
|
|
f, err := os.Open(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.ReadByte()
|
|
f.Close()
|
|
```
|
|
|
|
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
|
|
|
|
```Go
|
|
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中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進.
|
|
|
|
要特别註意短的變量聲明的作用域范圍, 考慮下面的程序, 它的目的是穫取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特别是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
|
|
|
```Go
|
|
var cwd string
|
|
|
|
func init() {
|
|
cwd, err := os.Getwd() // compile error: unused: cwd
|
|
if err != nil {
|
|
log.Fatalf("os.Getwd failed: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明爲局部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上面的代碼併不會更新包級聲明的 cwd 變量.
|
|
|
|
當前的編譯器將檢測到局部聲明的cwd併沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測併不可靠. 一些小的代碼變更, 例如增加一個局部cwd的打印語句, 就可能導致這種檢測失效.
|
|
|
|
```Go
|
|
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更加隱晦.
|
|
|
|
有許多方式可以避免齣現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式:
|
|
|
|
```Go
|
|
var cwd string
|
|
|
|
func init() {
|
|
var err error
|
|
cwd, err = os.Getwd()
|
|
if err != nil {
|
|
log.Fatalf("os.Getwd failed: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
我們已經看到包, 文件, 聲明和語句如何來表達一個程序結構. 在下面的兩個章節, 我們將探討數據的結構.
|
|
|
|
**譯註: 本章的詞法域和作用域概念有些混淆, 需要重譯一遍.**
|