作用域和生命期的概念可以借鑒 C 語言,但還有一些不太一樣的地方,需要單獨解釋。
1. 作用域與生命期
任何一門進階計算機語言都有作用域的概念,go 也不例外。
說到作用域,必然也會想到生命期。有些同學可能會把作用域和生命期劃上了等号,比如在 c 語言裡,在函數中聲明了局部變量
int x
,這個
x
的作用域就在函數體内,一旦執行完此函數,
x
也就銷毀了。
實際上,作用域和生命期有着本質的差別:
- 作用域是編譯期概念
- 生命期是運作時概念
換句話說,作用域限制了 object 的可見範圍。比如在某個函數裡聲明了變量
int x
,這個
x
就隻能在這個函數裡可見,在函數外面是無法使用它的,這一點是編譯器做出的限制。
如果你真的想在函數外面使用
x
,可不可以?在 c 語言裡其實可以通過指針将
x
位址傳出去,不過即使你有機會去修改它,可能會面臨程式 core dump 的風險。
但是在 go 裡我們知道,是允許将局部變量的位址傳到外面進行通路的。這意味着
x
的生命期被延長。
2. 作用域
其實你可以跳過這一節,但是最好不要。在講作用域前,先來明确文法塊和詞法塊的概念。
2.1 文法塊
在 go 裡,使用花括号括起來的部分,稱為文法塊。比如函數體,循環體。
func f() {
var a int = 5
var b int = 6
var c int
文法塊内部聲明的變量對外部是不可見的。
2.2 詞法塊
包含了一組聲明和語句的代碼片段,稱為詞法塊(不一定非得要花括号)。比如,文法塊是詞法塊的一個特例。還有一個特殊的例子就是整個程式所構成的源碼也是一個詞法塊,它比較特殊,有個單獨的名字,叫全局詞法塊。
還有很多例子,比如一個 package 構成包詞法塊,一個檔案構成檔案詞法塊,for、if、switch 語句所包含的詞法塊。
你需要注意的就是詞法塊的概念比文法塊更大,文法塊隻是詞法塊的一種。
2.3 作用域
那麼問題來了,作用域是什麼?首先明确一下,作用域表示的是範圍,接下來就是範圍大小的問題。
- 一條聲明語句所在的詞法塊的範圍決定了變量的作用域的範圍。
- goto, break, continue 後面的标簽的作用域,在目前函數内部。
- 作用域是可以嵌套的。
- 整個程式源碼所在的詞法塊決定了全局作用域範圍。
- 包級聲明位于全局作用域,但是是否能在其它包中通路取決于名字是否以大寫字母開頭。
注意:go 的 goto 和 c 的一樣,後面跟着标簽。go 的 break 和 continue 還有另外一種用法,就是 break 和 continue 後面也可以加标号,用在 for 循環裡。這是一種擴充的文法,以後遇到了再說。
當編譯器查找一個名字的時候,首先從最内層的作用域開始查找,一層一層往外找,直到全局作用域。如果最後在全局作用域還找不到,編譯器報錯。
3. 示例
下面的例子都在目錄
gopl/programstructure/scope
下面。下面的 4 個例子非常重要,請務必運作一遍。
- 例1
// demo01.go
package main
import "fmt"
var g int = 100
func test() {
var x int = 5
fmt.Println(x) // ok
}
func main() {
var local int = 10
fmt.Println(g) // ok
fmt.Println(local) // ok
fmt.Println(x) // not ok
- 例2
// demo02.go
package main
import "fmt"
func main() {
var x = "hello!"
for i := 0; i < len(x); i++ {
var x = x[i] // 這兩個 x 位于不同詞法塊,作用域也不同
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO"
上面的 for 語句建立了兩個作用域:
- 花括号括起來的那部分
- 循環初始化部分 + 條件表達式 + 自增語句 + 花括号部分
也就是說,作用域 2 包含了作用域 1. (
if else
條件語句也和
for
類似).
- 例 3
package main
import "fmt"
func main() {
if x := 5; x == 0 {
fmt.Println(x)
var z = 8
} else if y := 6; y == 0 {
fmt.Println(x)
} else {
fmt.Println(x)
fmt.Println(y)
fmt.Println(z) // not ok
分析一下,第一個 if 建立了三個作用域。
- 比較容易看出來的是 if 後面第一個花括号的部分。
- 第二部分就是第二個 else 語句開始到最後一個 else 結束。
- 第三部分則是從 if 開始一直到最後一個 else 結束的部分。
這很難看出來,但是上面的程式實際就是下面的程式的簡寫,我相信看完下面的代碼,你就明白了:
// demo04.go
package main
import "fmt"
func main() {
if x := 5; x == 0 {
fmt.Println(x)
var z = 8
} else {
if y := 6; y == 0 {
fmt.Println(x)
} else {
fmt.Println(x)
fmt.Println(y)
fmt.Println(z) // not ok,z 在這裡不可見
- 例 4
// demo05.go
package main
import "fmt"
func main() {
fmt.Println(g) // ok,盡管 g 聲明在後面,但是這裡仍然可以使用它
fmt.Println(local) // not ok.
var local int = 10
}
var g int = 100
這個例子想說明的是,包級聲明的變量,順序是無頭緊要的,你可以在聲明語句的上方就使用它。但是比包級作用域更小的作用域内聲明變量就必須要按照順序來(這種變量稱為局部變量),一個變量必須要在使用前聲明,否則會報錯。
4. 變量的生命期
生命期是運作時的概念。
對于包級變量來說,它的生命期和整個程式的運作周期是一緻的。程式運作結束,包級變量的生命期也随之結束。
但是局部變量的生命期是動态的:從變量被建立開始,直到該變量沒有被引用為止。但是變量沒有被引用,不意味着它的記憶體會被立即回收,可能會有一點延時,這取決于 go 的垃圾回收算法實作。
在 go 裡,函數裡的局部變量不一定是在棧上配置設定記憶體,使用 new 也不一定就非得在堆上配置設定記憶體。
舉個例子:
var global *int
func f() {
var x int = 1 // 這個 x 必須在堆上來配置設定記憶體
global = &x
}
func g() {
y := new(int) // 這個 y 可以在堆上配置設定記憶體,也可以在棧上配置設定記憶體
*y = 1
上面的
x
變量将自己的位址指派到全局變量
global 上
,這意味着在函數外面也能通路到
x
. 沒錯,
x
的生命期被延長為了整個程式的生命周期。
用 go 的專業術語是這樣說的:
x
escapes from
f
,翻譯成中文是:
x
從函數
f
中逃逸。
5. 總結
- 文法塊
- 詞法塊
- 作用域
- 生命期
- 逃逸