天天看點

023-作用域(Scope)與生命期(Lifetime)

作用域和生命期的概念可以借鑒 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 語句建立了兩個作用域:

  1. 花括号括起來的那部分
  2. 循環初始化部分 + 條件表達式 + 自增語句 + 花括号部分

也就是說,作用域 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 建立了三個作用域。

  1. 比較容易看出來的是 if 後面第一個花括号的部分。
  2. 第二部分就是第二個 else 語句開始到最後一個 else 結束。
  3. 第三部分則是從 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. 總結

  • 文法塊
  • 詞法塊
  • 作用域
  • 生命期
  • 逃逸